Переклад українською - Арсеній
Чеботарьов - Ніжин 2016
Оригінал http://danielwestheide.com/scala/neophytes.html
The
Neophyte's Guide to Scala
Частина 1: Екстрактори
Більше
ніж 50,000 людей записались на курс Odersky “Functional Programming Principles in Scala” на Coursera. Це гігантське
число розробників, для яких це могло бути першим контактом зі Scala,
функціональним програмуванням, або обома одночасно.
Якщо
ви читаєте це, можливо, ви один з них, або ви почали вивчати Scala з
інших причин. В жодному разі, якщо ви почали вивчати Scala, ви бажаєте
поглинути глибше в цю прекрасну мову, але вона все ще видається трохи
екзотичною або туманною для вас. Тоді ці статті, що я розпочинаю, саме
для вас.
Навіть
коли курс Coursera торкається загалу, який ви маєте знати про Scala,
існуючі обмеження часу роблять неможливим пояснити все в деталях. Як
результат, деякі можливості Scala можуть ввижатись вам як магія, якщо ви
нові в цій мові. Ви можете використовувати їх деяким чином, але ви не
повністю схопили як вони роблять, та, більш важливо, чому вони роблять
таким чином.
В
цій частині, та в наступних тижні, я хочу прояснити речі та видалити
знаки запитання. Я буду також пояснювати деякі з можливостей мови Scala
та бібліотеки, з якими я мав проблеми, коли я починав навчатись мові.
Частково тому, що я не знайшов гарних пояснень до них, але замість цього
натрапив на них в дикій природі. Коли це стосується, я також надаватиму
інструкції, як використовувати ці можливості ідіоматичним™ шляхом.
Досить
вступів. Перед тім, як я почну, майте на увазі, що хоча проходження
слухачем курсу Coursera не є попередньою вимогою для слідування цій
серії, мати знання Scala, що може бути отримано з лекцій, безумовно,
буде корисне. Та я буду часом посилатись на курс.
То
як насправді робить ця штуковина з порівнянням шаблонів?
В
курсі Coursera ви натрапите на дуже потужну можливість мови Scala: порівняння з шаблоном. Це дозволяє вам
декомпонувати дані структури даних, прикріпляючи значення, з
яких вони побудовані, до змінних. Однак це не уникальна до Scala ідея.
Інші помітні мови, в яких порівняння з шаблоном відіграють важливу
грають роль, це Haskell та Erlang, як для прикладу.
Якщо
ви слідували відео лекціям, ви бачили, що ви можете декомпонувати різні
типи структур даних з використанням порівнянням шаблонів, серед яких
списки, потоки, та любі примірники кейс класів. Чи є цей перелік
структур даних, що можна деструктурувати, є фіксованим, чи ви можете
розширити його якимось чином? Але спершу, як це в дійсності це дійсно
робить? Чи є деяка магія, що дозволяє вам писати речі як нижче?
Як
це з'ясовується, нічого такого немає. Або не багато. Причина, чому ви в
змозі писати код вище (не важливо, як багато сенсу має цей окремий
приклад), це існування так званих екстракторів.
В
найбільш широко застосованій формі, екстрактор має зворотню до
конструктора роль: коли останній створює об'єкти з наданого списку
параметрів, екстрактор виділяє параметри, з яких був стіорений об'єкт
при створенні.
Бібліотека
Scala має декілька попередньо визначені екстракторів, та ви скоро
побачите один з них. Кейс класи є особливими, бо Scala автоматично
створює компанйон об'єкт для них: об'єкт-синглтон, що містить не тільки
метод applyдля створення нових
примірників кейс класу, але також метод unapply – метод, що потрібно
реалізувати об'єкту, щоб він мав екстрактор.
Наш
перший екстрактор, йо!
Є
більше однієї можливої сигнатури для валідного метода unapply,
але ми почнемо з тих, що, найбільш вірогідно, будуть використовуватись.
Давайте уявимо, що наш класUser не є кейс класом взагалі, але
замість цього є трейтом, з двома класами, що розширюють його, та на
момент, він містить тільки одне поле:
Ми
бажаємо реалізовати екстрактори для класів FreeUser таPremiumUserв
відповідних об'єктах-компанйонах, саме так, як би зробила Scala в ціх
кейс класах. Якщо ваш екстрактор призначений тільки для виділення одного
параметру з наданого об'єкту, сигнатура метода unapplyвиглядає
так:
1
defunapply(object:S):Option[T]
Метод
очікує деякий об'єкт типу S, та повертає OptionтипуT,
що є типом параметра, який він екстрагує. Пам'ятайте, що Optionє безпечною альтернативою Scala
до існування значень null.
Про це буде окрема стаття, але наразі досить знати, що метод unapplyповертає або Some[T](якщо він може успішно
виділити параметр з даного об'єкта) абоNone,
що означає, що параметри не можуть бути виділені за правилами,
визначеними в реалізації екстрактора.
Але
ви звичайно не будете викликати їх напряму. Scala викликає метод
екстрактора unapply, якщо екстрактор використовується
як шаблон
екстрактора.
Якщо
результат виклику unapply єSome[T],
це означає, що шаблон співпадає, та виділене значення прив'язується до
змінної, задекларованої в шаблоні. Якщо це None,
це означає, що шаблон не співпав, та буде перевірятись наступна умова
співпадіння.
Давайте
використаємо наші екстрактори для порівняння з шаблоном:
Як
ви вже помітили, обоє наші екстрактори ніколи не повертають None.
Приклад показує, що це має більше сенсу, ніж може вважатись спочатку.
Якщо ви маєте об'єкт, що може бути одного типу, або іншого, ви можете
перевірити його тип, та розкласти його в той же час.
В
прикладі шаблонFreeUserне буде співпадати, оскільки
він очікує об'єкт іншого типу, що ми можемо передати йому. Оскільки він
бажає об'єкт типу FreeUser,
не типу PremiumUser,
цей екстрактор навіть ніколи не викликається. Однак значенняuser тепер передається до метода unapplyметода
компанйон-об'єкта PremiumUser, тому що цей екстрактор
використовується в другому шаблоні. Цей шаблон співпасть, а повернуте
значення буде прикріплене до параметруname.
Пізніше
в цій статті ми побачимо приклад екстрактора, що не завжди повертає Some[T].
Виділення
декількох значень
Тепер
уявімо, що наші класи такі, що ми бажаємо виділяти з них більше число
полів:
Якщо
шаблон екстрактора призначений для декомпозиції даної структури даних,
де більше одного параметра, сигнатура метода екстрактораunapplyвиглядає
так:
1
defunapply(object:S):Option[(T1, ..., Tn)]
Метод
очікує деякий об'єкт типу S, та повертає Option типуTupleN,
деN є кількістю параметів до виділення.
Давайте
адаптуємо наші екстрактори до модифікованих класів:
Тепер
ми можемо використовувати цей екстрактор для співпадіння шаблонів, точно
як ми робили в попередній версії:
123456
valuser:User=newFreeUser("Daniel",3000,0.7d)usermatch{caseFreeUser(name,_,p)=>if(p>0.75)name+", what can we do for you today?"else"Hello "+namecasePremiumUser(name,_)=>"Welcome back, dear "+name}
Логічний
екстарктор
Іноді
вам насправді не треба виділити параметри зі структури даних, з якою ви
робите співпадіння – замість цього ви просто бажаєте виконати деяку
логічну перевірку. В цьому випадку стане в нагоді третя, та остання з
існуючих сигнатур метода unapply, що очікує значення значення
типуS та повертає Boolean:
1
defunapply(object:S):Boolean
Використаний
в шаблоні, він дасть співпадіння, якщо екстрактор поверне true.
Інакше буде спробоване наступне порівняння.
В
попередньому прикладі ми мали деяку логіку, що перевіряла, чи простий
користувач може бути підозрюваним в якості скорого кандидата на апгрейд.
Давайте покладемо цю логіку в наш власний логічний екстрактор:
Як
ви можете бачити, не є необхідним для екстрактора знаходитись в
об'єкті-компанйоні класа, до якого він стосується. Використання цього
логічного екстрактора є простим:
Цей
приклад показує, що логічний екстратор використовується простою
передачей йому пустого списку параметрів, що має сенс, бо насправді нема
ніякого виділення жодних параметрів для прикріплення до змінних.
Є
одна інша особливість цього приклада: я претендую на те, що наша
функціональна функція initiateSpamProgram очікує екземпляр FreeUser,
оскільки преміум користувачі ніколи не отримуватимуть спам. Однак, наше
співпадіння виконується з шаблоном проти любого типуUser,
так що я не можу передавати userдо функціїinitiateSpamProgram– не без огидного кастингу
типів.
На
щастя, порівняння шаблонів Scala дозволяє прикріпляти до змінної
значення, що співпадають, також з використанням типу, що очікує
використаний екстрактор. Це робиться за допомогою оператора@.
Оскільки наш екстракторpremiumCandidate
очікує примірникFreeUser,
ми маємо, таким чином, прикріплення співпавшого значення до змінноїfreeUserтипуFreeUser.
Персонально
я не дуже багато використовував логічні екстрактори, але добре знати, що
вони існують, бо раніше або пізніше ви опинитесь в ситуації, коли вони
знадобляться.
Шаблони
інфіксних операторів
Якщо
ви слідували за Scala курсом на Coursera, ви знаєте, що ви можете
деструктувати списки та потоки в спосіб, що є спорідненим до того, як ви
створюєте їх, використовуючи оператор cons ,:: або#::відповідно:
Можливо
ви дивуєтесь, як це можливо. Відповідь полягає в тому, що в якості
альтернативи до нотації шаблонів екстракторів, що ми вже бачили, Scala
також дозволяє екстракторам бути задіяними в інфіксній нотації. Таким
чином, замість написання e(p1,
p2), деe є екстрактор, та p1 таp2є параметри, що будуть
виділені з наданої структури даних, завжди можливо записати p1
e p2.
Таким
чиномшаблон
інфіксної операціїhead
#:: tail може також бути
записаний як #::(head,
tail), та наш екстрактор PremiumUserтакож може бут використаний в
шаблоні, що читаєьсяname
PremiumUser score. Однак, це не щось, що вам треба робити на
практиці. Використання шаблону інфіксного оператора рекомендований
тільки для екстракторів, що дійсно призначені для читання як оператори,
що вірне для операторів cons List таStream,
але безсумнівно не для нашого
екстрактора PremiumUser.
Ближчий
погляд на екстрактор Stream
Навіть
зважаючи, що немає нічого особливого в тому, як екстрактор #:: може бути задіяний в порівнянні
шаблонів, давайте поглянемо на нього, щоб краще зрозуміти, що
відбувається в нашому коді порівняння. Також це гарний приклад
екстрактора, що, в залежності від стану переданої структури даних, може
повертати None, і, таким чином, не співпадати.
Ось
повний екстрактор, взятий з джерел Scala 2.9.2:
взяте
з scala/collection/immutable/Stream.scala, (c) 2003-2011, LAMP/EPFL
Якщо
даний примірникStream пустий, він повертаєNone.
Таким чином, case
head #:: tail не співпадає
для пустого потоку. Інакше повертається Tuple2,
перший елемент якого є головою потоку, тоді як другий елемент кортежу є
хвіст, що знову є Stream.
Таким чином, case
head #:: tail буде
співпадати для потока з одного, або більше, елементів. Якщо
він має тільки один елемент, tail буде прикріплений до пустого
потока.
Щоб
зрозуміти, як цей екстрактор робить для нашого приклада співпадіння,
давайте перепишемо цей приклад, йдучи від шаблону інфіксного операнду до
звичайної нотації:
Спершу
екстрактор викликається для початкового потокуxs, що переданий в блок порівняння.
Екстрактор повертає Some((xs.head,
xs.tail)), так що firstприв'язане до 58,
тоді як хвіст xsзнову передається в
екстрактор, що використовується знову в першому порівнянні. Знову, він
повертає голову та хвіст, як Tuple2, огорнуте в Some,
так що second прив'язане до значення 43,
тодя як хвіст прив'язується до підстановочного символа_, і, таким чином, відкидається.
Використання
екстракторів
Так
що, коли і як ви, насправді, повинні використовувати власні ексрактори,
особливо беручи до уваги, що ви можете отримати деякі корисні
екстрактори задурно, якщо використовуєте кейс класи?
Хоча
деякі люди вказують, що використання кейс класів, та застосування до них
співпадіння шаблонів з ними ламає інкапсуляцію, пов'язуючи спосіб, що ви
порівнюєте дані, з конкретним представленням, цей критицизим зазвичай
походить від об'єктно-орієнтованого погляду на речі. Це гарна ідея, якщо
ви бажаєте робити функціональне програмування в Scala, використовувати
кейс класи як алгебраїчні типи даних (ADT), що містять тільки дані, але жодної
поведінки.
Зазвичай,
використання ваших власних екстракторів необхідне тільки в випадку, якщо
ви бажаєте виділяти з типу те, над чим ви не маєте влади, або якщо вам
потрібні додаткові шляхи співпадіння з шаблоном проти певних даних.
Наприклад, загальним використанням екстракторів є виділення осмислених
значень з деякого рядка. В якості вправи подумайте, як ви повинні
реалізовати та використовувати URLExtractor, що приймає Stringпредставлення URL.
Висновок
В
цій першій частині з серії ми розглянули екстрактори, робочу конячку за
співпадінням шаблонів Scala. Ви навчились, як реалізувати ваші власні
екстрактори, та як реалізація екстрактора пов'язана з його використанням
в шаблоні.
Ми
не розглянули все, що можна сказати про екстрактори, тому що ця стаття
вже є досить довгою. В наступній частині з цієї серії я збираюсь
повернутись до екстракторів, розповідаючи, як реалізувати їх, якщо ви
бажаєте число змінних екстрагованих параметрів в шаблоні.
Дайте
мені знати, чи ця стаття була корисною для вас, або дещо не було вам
зрозуміло.
Частина
2: Екстракція послідовностей
В
першій частині цієї серії ми вивчили, як втілити наші власні
екстрактори, та як ці екстрактори можуть використовуватись для
порівняння шаблонів. Однак ми дискутували тільки екстрактори, що
дозволяють вам деструктурувати даний об'єкт в фіксоване число
параметрів. Але для деяких різновидів структур даних Scala дозволяє вам
виконати порівняння шаблонів, очікуючи довільне число виділених
параметрів.
Наприклад,
ви можете використовувати шаблон, що співпадає тільки зі списком, що
містить рівно два елементи, або зі списком з рівно трьох елементів:
Тут
перший шаблон співпадає, прикріплюючі перші два елементи до
змінних a таb,
та повністю ігноруючи решту списку, безвідносно до того, як багато
лишилось тих елеметнів.
Зрозуміло,
екстрактори для ціх типів шаблонів не можуть бути реалізовані у спосіб,
що я виклав в першій статті. Нам потрібен шлях вказати, що екстрактор
приймає об'єкт певного типу, та деструктує його в послідовність
виділених значень, де довжина цієї послідовності невідома під час
компіляції.
ВведемоunapplySeq,
метод екстарктора, що дозволяє робити семе це. Давайте поглянемо на одну
з можливих сігнатур метода:
1
defunapplySeq(object:S):Option[Seq[T]]
Він
очікує об'єкт типу S, та повертає або None,
якщо об'єкт зовсім не співпадає, або послідовність виділеного типу T,
огорнутого в Some.
Приклад:
виділення наданих імен
Давайте
використаємо цей тип методів екстрактора в наступному вигаданому
прикладі. Скажімо, в деякій частині застосування ми отримуємо ім'я
користувача як String.
Цей рядок може містити друге, або третє ім'я користувача, якщо він має
більше одного наданого імені. Таким чином, можливі значення можуть
бути "Daniel",
або"Catherina
Johanna", або"Matthew
John Michael". Ми бажаємо мати змогу порівняти ці імена,
виділяючи та прив'язуючи окремі надані імена.
Ось
дуже проста реалізація екстрактора в термінах метода unapplySeq, що дозволить нам зробити це:
Беручи String, що містить одне або більше імен, це
буде виділяти їх як послідовність. Якщо вхідне ім'я не містить
щонайменьше одного наданого імені, цей екстрактор буде повертати None,
і, таким чином, шаблон в цьому використаному екстракторі не буде
співпадати з рядком.
Тепер
ви можете надати наш новий екстрактор до тесту:
1234
defgreetWithFirstName(name:String)=namematch{caseGivenNames(firstName,_*)=>"Good morning, "+firstName+"!"case_=>"Welcome! Please make sure to fill in your name!"}
Цей
вправний малий метод повертає привітання для наданого імені, ігноруючи
все, крім першого імені.greetWithFirstName("Daniel")буде повертати "Good
morning, Daniel!", аgreetWithFirstName("Catherina
Johanna") поверне"Good
morning, Catherina!"
Комбінація
екстракції фіксованих та змінних параметрів
Іноді
ви маєде деякі значення, що мають бути виділені, що ви знаєте під час
компіляції, плюс додаткові опціональні послідовності значень.
Давайте
уявімо в нашому прикладі, що вхідне ім'я містить повне ім'я, не тільки
надане. Можливі значення можуть бути "John
Doe" або"Catherina
Johanna Peterson". Ми бажаємо порівнювати такі рядки з
використанням шаблонів, що завжди прикріплює останнє ім'я людини до
першої змінної в шаблоні, та перше ім'я до другої змінної, за якими
слідує довільне число додаткових наданих імен.
Це
може бути досягнено за допомогою невеликої модифікцїі методаunapplySeq,
з використанням іншої сигнатури метода:
Як
ви можете бачити, unapplySeq також може повертати Option зTupleN,
де останній елемент кортежу має бути послідовністю, що містить змінну
частину виділених значень. Ця сигнатура методу повинна бути дещо
знайомою, тому що вона подібна до однієї з можливих сигнатур
методу unapply, що я показував на минулому
тижні.
Подивіться
ближче на повертаємий тип, та на конструкцію Some.
Наш метод повертає Option з Tuple3.
Цей кортеж стоврений за допомогою синтаксиса Scala дял літералів, просто
покладаючи три елементи – останнє ім'я, перше ім'я та послідовність
додаткових наданих імен – в парі дужок.
Якщо
цей екстрактор використовується в шаблоні, шаблон буде співпадати, якщо
щонайменьше останнє ім'я міститься в наданому вхідному рядку.
Послідовність додаткових наданих імен створюється, відкидаючи перший та
останній елемент з послідовності імен.
Ми
можемо використовувати цей екстрактор для реалізації альтернативного
метода привітання:
1234
defgreet(fullName:String)=fullNamematch{caseNames(lastName,firstName,_*)=>"Good morning, "+firstName+" "+lastName+"!"case_=>"Welcome! Please make sure to fill in your name!"}
Почувайтесь
вільним погратись з цім в REPL або на робочому листі.
Підсумок
В
цій главі ми навчились, як реалізовати та використовувати екстрактори,
що повертають послідовності виділених значень змінної довжини.
Екстрактори є досить потужним механізмом. Вони часто можуть бути
повторно використані в гнучкий спосіб, та провадити потужний шлях для
розширення типів шаблонів, з якими ви можете робити порівняння.
Ми
будемо переглядати екстрактори в навчальному прикладі наприкінці кінця
цієї серії. В наступній частині, однак, я надам огляд різних шляхів,
яким шаблони можуть застосовуватись в коді Scala – є більше варіантів
порівнянь з шаблоном, ніж ви вже бачили до тепер.
Оновлення,
24.01.2013: Я оновив приклад кода, реалізуючи екстрактор
GivenNames. Дякую Christophe Bliard, що вказав на помилку.
Частина
3. Шаблони повсюди
В
перших частинах цієї серії я витратив деякий час, пояснюючи, що
насправді відбувається, коли ви деструктуєте примірник кейс класу в
шаблоні, та як писати ваші власні екстракторі, дозволяючи вам
деструктувати любі типи об'єктів, в любий бажаний спосіб.
Тепер
настав час зробити огляд, де шаблони можуть насправді використовуватись
в вашому Scala коді, оскільки до тепер ви бачили тільки одну з
різноманітних шляхів для використання шаблонів. От де ми є!
Вирази
порівняння шаблонів
Одне
місце, де можуть з'являтись шаблони, це вирази
поірвняння шаблонів. В такій спосіб використання шаблонів має
бути дуже вам знайомим після проходження курсу Scala на Coursera, та
впродовж цієї серії. Ви маєте неякий виразe,
за який слідує ключове слово matchта
блок, що може містити любе число випадків. Випадок, в свою чергу,
складається з ключового слова case, за яким слідує шаблон, та,
опціонально, зліва частина охоронця, плюс блок зправа, що буде
виконано, якщо шаблон співпадає.
Ось
простий приклад, що використовує шаблон, та охоронця в одному з
випадків:
123456
caseclassPlayer(name:String,score:Int)defprintMessage(player:Player)=playermatch{casePlayer(_,score)ifscore>100000=>println("Get a job, dude!")casePlayer(name,_)=>println("Hey "+name+", nice to see you again!")}
Метод printMessage має тип Unit,
його єдине призначення полягає в побічному ефекті, наразі в друку
повідомлення. Важливо пам'ятати, що ви не маєте використовувати
порівняння з шаблоном, як ви це робили з використанням твердження switch
в мовах, як Java. Те, що ми тут використовуємо, називається виразомпорівняння з шаблоном не
даремно. Їх повернуте значення є те, що повертається з блоку, який
відповідає до першого співпавшого шаблону.
Звичайно,
є гарною ідеєю отримати зиск з цього, оскільки це дозволяє вам розділити
дві речі, що насправді не належать одне до одного, роблячи простішим
тестування вашого коду, також. Ми також отримали приклад вище, наступним
чином:
12345
defmessage(player:Player)=playermatch{casePlayer(_,score)ifscore>100000=>"Get a job, dude!"casePlayer(name,_)=>"Hey "+name+", nice to see you again!"}defprintMessage(player:Player)=println(message(player))
Тепер,
ми маємо виділити методmessage, що повертає типString.
Це, загалом, чиста функція, що повертає вираз порівняння шаблону. Ви
можете також зберігти результат такого порівняння, як значення, або,
звичайно, присвоїти його змінній.
Шаблони
в визначеннях значення
Інше
місце, де в Scala може трапитись порівняння з шаблоном, є ліва частина визначення
значення(та в визначенні
змінної, в цьому значенні, але ми бажаємо написати наш Scala код
в функціональному стилі, так що ми не бажаємо бачити багато визначень
змінних в цій серії). Давайте вважати, що ми маємо метод, що повертає
поточного гравця. Ми будемо використовувати умовну реалізацію, що
постійно повертає одного гравця:
Якщо
ви знаєте Python, ви, можливо, знайомі з можливістю, відомою як розпакування
послідовності. Факт, що ви можете використовувати любі шаблони
зліва від визначення значення, або визначення змінної, дозволяє вам
писати ваш Scala код в подібному стилі. Ми можемо змінити код вище, та
деструктувати наданого поточного гравця , при тому присвоювуючи його до
лівої сторони:
Ви
можете робити це з кожним шаблоном, але, загалом, є гарною ідеєю,
щоб переконатись, що ваш шаблон завжди співпадає. Інакше ви станете
свідком виключення під час виконання. Наприклад, наступний код
проблематичним. Методscores повертає список досягнень. В
нашому коді нижче, це метод просто повертає пустий список, щоб
проілюструвати цю проблему.
123
defscores:List[Int]=List()valbest::rest=scoresprintln("The score of our champion is "+best)
Отакої,
ми отримали MatchError.
Здається, що наша гра не є успішною, кінець кінцем, не маючи будь-яких
рейтингів.
Безпечний,
та дуже зручний, спосіб використання шаблонів в цей спосіб є
деконструкцію кейс класів, чий тип відомий під час компіляції. Також,
коли робимо з кортежами, це робить ваш код значно більш читабельним.
Давайте уявімо, що ми маємо функцію, що повертає ім'я гравця, та його
бали, в вигляді кортежа, не використовуючи клас Player,
що ми використовували до цього:
1
defgameResult():(String,Int)=("Daniel",3500)
Доступ
до полів кортежа завжди виглядає дуже незручним:
Шаблони
також мають дуже впливове місце в for осягненнях. Для початку, for
осягнення може також містити визначення значень. Та все, що ви вивчили
щодо використання шаблонів в лівій стороні визначень значень, остається
вірним для визначень значень в осягненнях. Таким чином, якщо ми маємо
колекцію результатів гравців, та бажаємо скласти їх рейтинг, що в нашій
грі є тільки набором імен гравців, що перетнули деякий рубіж по очках,
ми можемо зробити це в дуже зрозумілий спосіб завдяки осяжності:
Результатом
є List("Melissa",
"John"), оскільки перший гравець на задовільняє умові охоронця.
Це
може бути записане навіть більш стисло, оскільки в for осяжності ліва
сторона генераторатакож є шаблоном. Так що,
замість спочатку присвоювати кожний результат гри доresult,
ми можемо напряму деструктувати результат в лівій стороні генератора:
В
цьому прикладі шаблон (name,
score) завжди співпадає,
так що, якщо б не було твердження охоронця, if
(score > 5000), for осяжність була б еквівалентною до
простого відображення з кортежів на імена гравців, без жодної
фільтрації.
Важливо
знати, що шаблони в лівій стороні генератора можуть все ще бути
використані для цілей фільтрації – якщо шаблон в лівій частині не
співпадає, відповідний елемент буде відсіяно.
Щоб
продемонструвати це, давайте уявімо, що ми маємо послідовність списків,
та ми бажаємо повернути розміри для непорожніх списків. Це означає, що
ми маємо відфільтрувати всі непорожні списки, та потім повернути розміри
тих, що залишились. Ось одне з рішень:
Шаблон
зліва генератора не співпадає з пустими списками. Це не викликає MatchError,
але призводить до виключення пустих списків. Таким чином, ми
отримаємо List(3,
2).
Шабони
та for осяжності є дуже природним та потужним поєднанням, та якщо ви
проробите зі Scala деякий час, ви побачите, що ви використовуєти їх
досить багато.
Анонімні
функції
Нарешті,
шаблони можуть використовуватись для визначення анонімних функцій. Якщо
ви будь-коли використовували блок catch, щоб мати справу з виключеннями
Scala, тоді ви використовували цю можливість. Функції порівняння з
шаблоном є привідом для окремого блог посту, оскільки є багато чого, що
варто про них сказати. Таким чином, я уникну заглиблення в це
використання шабонів, в цьому розділі, та замість цього залишаю вам
обіцянку повернутись до цього в наступній частині цієї серії.
Оновлення: Виправдено помилку в очікуваному
результаті hallOfFamefor осяжності. Дякую Rajiv
що вказав на це.
Частина
4: Анонімні функції порівняння шаблонів
В
попередній частині я надав огляд різних шляхів, як порівняння шаблонів
може бути застосоване в Scala, завершуючи коротким спогадом про анонімні
функції, як інше місце, де шаблони можуть знайти своє застосування. В
цьому пості ми збираємось покалсти уважний погляд на можливості, що
відкриває визначення анонімних функцій таким чином.
Якщо
ви брали цчасть в курсі Scala на Coursera,
або кодували на Scala деякий час, ви, вірогідно, писали анонімні функції
на регулярній основі. Наприклад, беручи список наз пісень, що ви бажаєте
перетворити на нижній реєстр для індексу пошуку, ви, можливо, визначали
анонімну функцію, що ви передавали до методу map,
таким чином:
12
valsongTitles=List("The White Hare","Childe the Hunter","Take no Rogues")songTitles.map(t=>t.toLowerCase)
Або,
якщо ви бажаєте ще коротше, ви можете нормалізувати функцію, таким
чином, з використанням синтаксису заміщувача making use of Scala:
1
songTitles.map(_.toLowerCase)
Доки
все гаразд. Однак давайте подивимось, як цей синтаксис виступає в дещо
іншому прикладі: ми маємо послідовність пар, кожна представляє слово та
його частоту в деякому тексті. Наша ціль відфільтрувати ці пари, чия
частота нижче або виде деякого значення, та потім повернути залишок
слів, без їх відповідних частот. нам треба написати функціюwordsWithoutOutliers(wordFrequencies:
Seq[(String, Int)]): Seq[String].
Наше
початкове рішення використовує методи filter таmap,
передаючи анонімні функції до них, з використанням знайомого синтаксису:
Це
рішення має декілька проблем. Перша є тільки естетичною – доступ до
полів кортежу є огидним, як на мене. Якщо ми бажаємо тільки
деструктувати пару, ми можемо зробити цей код трохи більш приємним, та,
можливо, більше читабельним.
На
щастя, Scala провадить альтернативний шлях написання анонімних
функції: анонімна
функція порівняння з шаблономє анонімною функцією, що визначена як блок, що складається з
послідовності випадків, заточений, як завжди, в фігурні дужки, але без
ключового слова match перед блоком. Давайте
перепишемо нашу функцію з застосуванням цієї нотації:
В
цьому прикладі ми використовували тільки один випадок в кожній з наших
анонімних функцій, оскільки ми знаємо, що цей випадок завжди
співпадатиме – ми просто декопонували структуру даних, чий тип вже
відомий під час компіляції, так що тут не може відбутись нічого
поганого. Це дуже загальний спосіб використання порівняння шаблону з
анонімними функціями.
Якщо
ви будете намагатись присвоїти ці анонімні функції до значень, ви
побачите, що вони матимуть очікуваний тип:
Будь
ласка, зауважте, що вам треба вказати тип значення, компілятор Scala не
може вивести його з анонімних функцій порівняння з шаблоном.
Звичайно,
ніщо не заважає вам визначати більш складні послідовності випадків.
Однак, якщо ви визначаєте анонімну функцію таким чином, та бажаєте
передати ії в якусь іншу функцію, як в нашому прикладі,ви маєте
переконатись, що для всіх можливих вводів співпадає з одним з ваших
випадків, так, щоб ця ваша анонімна функція завжди повертала значення.
Інакше ви ризикуєте отримати MatchError під час виконання.
Часткові
функції
Іноді,
однак, функція, що визначена тільки для специфічних значень вводу - це
саме те, що вам потрібне. Фактично, таа функція може допомогти нам
подолати іншу проблему, що все ще не вирішена, з нашою поточною
реалізацією функціїwordsWithoutOutliers:
ми спершу фільтруємо надану послідовність, та потім відображуємо
елементи, що залишились. Якщо ми можемо утрусити це до рішення, що
ітерує по послідовності тільки один раз, це не тільки зменшить цикли
CPU, але також зробить код меньшим, та, нарешті, більш зрозумілим.
Якщо
ви передивлялись Scala API для колекцій, ви могли помітити метод з
назвоюcollect,
що, для Seq[A]має
наступну сигнатуру, :
1
defcollect[B](pf:PartialFunction[A, B])
Цей
метод повертає нову послідовність, застосовуючи надану часткову
функцціюдо всіх
елементів – часткова функція обоє, фільтрує та відображує послідовність.
Так
що таке часткова функція? Коротко, це унарна функція, що, як відомо,
визначена тільки для певних вхідних значень, та дозволяє клієнтам
перевіряти, чи вона визначена для специфічного вхідного значення.
З
цього боку, трейт PartialFunction провадить метод isDefinedAt.
Фактично, типPartialFunction[-A,
+B] поширює тип(A)
=> B(що
також може бути записано якFunction1[A,
B]), та анонімна функція порівняння шаблону є завжди типуPartialFunction.
Через
цю ієрархію наслідування, передача анонімної функції порівняння з
шаблоном до метода, що очікуєFunction1,
як map абоfilter,
є досить гарним, доки ця функція визначена для всіх вхідних значень,
тобто завжди існує співпадаючий випадок.
Методcollect, однак, особливо очікує PartialFunction[A,
B], що може бути визначена
для всіх вхідних значень, та знає, як саме мати справу з цім випадком.
Для кожного елементу в послідовності вона, спершу, перевіряє, чи
часткова функція визначена для нього, викликаючи isDefinedAt на частково визначеній функції.
Якщо це повертає false,
елемент ігнорується. В іншому випадку, результат застосування часткової
функції до елементу додається до результуючої послідовності.
Давайте
спочатку визначимо часткову функцію, що ми бажаємо використати для
рефакторингу нашої функції wordsWithoutOutliers, щоб задіятиcollect:
Ми
додали твердження захисника до нашого випадку, так що ця функція не буде
визначена до для пар слово/частота, чия частота не в потрібному
диапазоні.
Замість
використання синтаксису для анонімних фунцій порівняння шаблонів, ми
можемо визначити цю часткову функцію, явно розширивши трейтPartialFunction:
Однак,
зазвичай, ви побажаєте використовувати значно більш стислий синтаксис
анонімних функцій.
Тепер,
якщо ми передали нашу часткову функцію до метода map,
це буде досить гарно компілюватись, але призведе доMatchError під час використання, оскільки
наша часткова функція не визначена для всіх можливих вхідних значень,
дякуючи до доданих тверджень захисників:
1
wordFrequencies.map(pf)// will throw a MatchError
Однак
ми можемо передати цю часткову функцію до метода collect,
та він буде фільтрувати та відображувати послідовність, як очікується:
Результат
цього такий же самий , як той, що дає наша поточна реалізація wordsWithoutOutliers, коли ми передаємо нашу умовну
послідовність wordFrequenciesдо
неї. Таким чином, давайте перепишемо цю функцію:
Часткові
функції мають деякі інші, дуже важливі функції. Наприклад, вони
провадять умови для сціплення, дозволяючи милу функціональну
альтернативу ланцюгам шалонів відповідальності, відомим з об'єкт-орієнтовного
програмування. Це, однак, буде приводом для наступного посту в
серії, де я збираюсь розглянути питання функціональної здатності до
композиції.
Часткова
функціональність є також накутним елементом багатьох бібліотек Scala та
API. Наприклад, спосіб, яким актори Akka обробляють надіслані ним повідомлення,
визначені в термінах часткових функцій. Таким чином, це досить важливо
знати та розуміти цю концепцію.
Підсумок
І
цій частині ми дослідили альтернативний шлях визначення анонімних
функцій, зокрема для послідовності випадків, що відкриває деякі
милі можливості деструкції, в дещо компактному вигляді. Більше того, ми
занурились в тему часткових функцій, демонструючи їх корисність в
аспекті простого випадку.
В
наступній главі я маю намір копати глибше в вездесущий тип Option,
пояснюючи причини його існування, та його використання найкращим чином.
Будь
ласка, дайте мені знати, якщо ви маєте жодні запитання або дещо. Чи є
деякі теми, що ви бажаєте побачити в наступних статтях?
Частина
5: Тип Option
На
протязі останніх тижнів ми просувались далі, та розлянули більшість
підгрунтя, що відноситься до досить складних матерій, зокрема порівняння
з шаблонами та екстракторів. Час вповільнитись, та поглянути на більш
фундаментальні ідеосинкразичні речі Scala: типOption.
Якщо
ви брали курс Scala на Coursera, ви вже отримали коротке введення в цей
тип, та бачили його в дії в MapAPI.
В
цій серії ми також використовували його, коли реалізували свої власні
екстрактори.
Але
все ще залишилось багато, що треба прояснити про нього. Ви, можливо,
дивувались, що це все за галас кругом, що такого значно кращого щодо
опцій, ніж інших шляхів представлення відсутності значення. Ви також,
можливо, не в курсі, як насправді робити з типом Option в вашому власному коді. Ціль
цієї частини серії позбутися всіх ціх знаків запитань, та навчитись
всьому, що насправді треба знати щодоOption, в якості завзятого новачка Scala.
Базова
ідея
Якщо
ви взагалі робили з Java в минулому, дуже вірогідно, що ви мали в деякий
момент NullPointerException
(інші мови будуть викликати подібні помилки в такому ж випадку). Можливо
це трапляється, оскільки деякий метод повертає null, коли ви не очікували його, і, таким
чином, не розглядали таку можливість в клієнтському коді.
Значення null часто невірно використовується для
представлення відсутнього опціонального значення.
Деякі
мови трактують значення null в осообливий спосіб, або дозволяють
вам безпечно робити зі значеннями, що можуть бути null.
Наприклад, Groovy має null-безпечний оператор для доступу до
властивостей, так що foo?.bar?.baz не буде викликати виключення, якщо абоfoo, або його властивістьbar єnull,
замість цього напряму повертаючи null.
Однак ви схибите, якщо ви забудете використати цей оператор, та ніщо вас
не змушує до цього.
Clojure
загалом
трактує своє значення nil як пусту річ, тобто як пустий
список, якщо доступ іде як до списка, або як пусту мапу, якщо доступ іде
як до мапи. Це означає, що значення nil підіймається вгору по ієрархії
викликів. Дуже часто все гаразд, але іноді це призводить до
виключення значно вище в ієрархії викликів, де деякий шматок кода не є
nil-дружнім в кінці кінців.
Scala
намагається вирішити проблему позбавлення від значень null, разом провадячи свій власний тип для
представлення опціональних значень, тобто значень, що можуть
бути присутніми, або ні: трейтOption[A].
Option[A] є контейнером для опціонального
значення типу A.
Якщо значення типу A присутнє, Option[A] є примірником Some[A],
що містить присутнє значення типу A. Якщо значення відсутнє, Option[A] є об'єктом None.
Затверджуючи,
що значення може бути, або не бути присутнім, на
рівні типу, ви, та всі інши розробники, що роблять з вашим кодом,
змушені компілятором мати справу з ціма можливостями. Немає способу,
коли ви випадково будете покладатись на присутність значення, що
насправді опціональне.
Option є обов'язковим! Не
використовуйте null для позначення, що опціональне
значення відсутнє.
Створення
опції
Звичайно,
ви можете просто створити Option[A] для існуючого значення, напряму
уособивши кейс клас Some:
1
valgreeting:Option[String]=Some("Hello world")
Або,
якщо ви знаєте, що значення відсутнє, ви просто присвоюєте або
повертаєте об'єкт None:
1
valgreeting:Option[String]=None
Однак,
час від часу вам треба взаємодіяти з бібліотеками Java, або кодом на
других мовах JVM, що щасливо використовують null для позначення відсутніх значень.
З цієї причини об'єкт-компанйон Option провадить метод-фабрику, що створюєNone, якщо параметр єnull,
інакше параметр огортається в Some:
12
valabsentGreeting:Option[String]=Option(null)// absentGreeting буде NonevalpresentGreeting:Option[String]=Option("Hello!")// presentGreeting буде Some("Hello!")
Робота
з опціональними значеннями
Це
все мило, але як, насправді, робити з опціональними значеннями? Прийшов
час для прикладу. Давайте зробимо щось нудне, так, щоб зконцентруватись
на важливих речах.
Уявімо,
що ви робите на одному з тих жахливих стартапів , і однією з перших
речей, щ вам треба, є реалізація репозитарію користувачів. Нам треба
бути в змозі знайти користувача за його унікальним id. Іноді запити
приходять з деякими фіктивними id. Це закликає до типу повернення Option[User] для нашого метода пошуку.
Умовна реалізація нашого репозитарію користувачів може виглядати так:
Тепер,
якщо ви отримали примірник Option[User] відUserRepository, та потребуєте зробити щось з ним,
як це зробити?
Одним
шляхом може бути перевірка, чи значення присутнє силами метода isDefinedвашої опції, та, якщо це так,
отримати значення через методget:
1234
valuser1=UserRepository.findById(1)if(user1.isDefined){println(user1.get.firstName)}// will print "John"
Це
дуже подібно то того, як робить тип Optional бібліотеки Guava в Java. Якщо
ви вважаєте, що це довге, та очікуєте дещо більш елегантне від Scala, ви
на вірному шляху. Більш важливо, якщо ви використовуєте get,
ви можете забути про перевірку isDefined перед цім, що призведе до
виключення під час виконання, так що ви не отримаєте значно більше, ніж
від використанняnull.
Ви
повинні триматись подалі від цього шляху доступу до опцій, тільки як це
можливо!
Провадження
значення по замовчанню
Дуже
часто ви бажаєте робити з вдкатом, або значенням по замовчанню, в
випадку опціонального значення, що відсутнє. Це випадок використання є
дуже гарно виражений методомgetOrElse, визначений для Option:
12
valuser=User(2,"Johanna","Doe",30,None)println("Gender: "+user.gender.getOrElse("not specified"))// will print "not specified"
Будь
ласка зауважте, що значення по замовчанню, яке ви можете вказати як
параметр до методуgetOrElse, є параметром за ім'ям, що
означає, що він обчислюється тільки якщо опція, на якій ви
викликаєте getOrElse насправдіNone.
Таким чином, немає потреби турбуватись, якщо створення значення по
замовчанню є коштовним з однієї або іншої причини – це буде траплятись,
тільки якщо значення по замовчанню буде насправді потрібним.
Порівняння
з шаблоном
Some є кейс класом, так що чудово можливо
використовувати його в шаблоні, буде це в звичайному виразі порівняння
з шаблоном, або якомусь іншому дозволеному місці. Давайте
перепишемо приклад вище, з використанням порівняння з шаблоном:
12345
valuser=User(2,"Johanna","Doe",30,None)user.gendermatch{caseSome(gender)=>println("Gender: "+gender)caseNone=>println("Gender: not specified")}
Або,
якщо ви бажаєте виключити дублікат твердженняprintln, та використати факт, що ми робитмо
з виразом
порявняння з шаблоном:
Можливо
ви помітили, що порівняння з шаблоном примірника Optionдосить
балакуче, через що зазвичай не є ідиоматичним обробляти опції в цей
спосіб. Таким чином, навіть якщо ви всі захоплені щодо порівняння
шаблонів, спробуйте використовувати альтернативи при роботі з опціями.
Є
один досить елегантний шлях використання шаблонів з опціями, що ви
навчитесь коли дійдете до розділу нижче про осяжності.
Опції
можна розглядати як колекції
Доки
ви не бачили багато елегантних ідеоматичних шляхів роботи з опціями.
Тепер ми займемось цім.
Я
завжди казав, що Option[A] є контейнером для значення типуA.
Більш точно, ви можете думати про це, як про різновид колекції – деяка
особлива сніжинка колекції, що містить або нуль елементів, або точно
один елемент типуA.
Це дуже потужна ідея!
Навіть,
думаючи на рівні типу, якщо Option не є типом колекції в Scala,
опції ідуть з усіма благами, що ви цінуєте в колекціях Scala, якList,Set,таке інше – та якщо вам дійно
це треба, ви навіт можете трансформувати опції, наприклад, в List.
Так
що це дозволяє нам робити?
Виконання
побічних ефектів за наявності значення
Якщо
ви бажаєте виконати деякий побічний ефект за наявності опціонального
значення, методforeach, відомий з колекцій Scala
стане вам у пригоді:
1
UserRepository.findById(2).foreach(user=>println(user.firstName))// prints "Johanna"
Функція,
передана до foreach, буде викликана один раз, якщо Option єSome,
або жодного разу, якщо це None.
Відображення
(мапи) опцій
Дійсно
гарна річ щодо опцій в якості колекцій в тому, що ви можете робити з
ними в дуже функціональний спосіб, та цей спосіб співпадає з тим, як
виробите зі списками, наборами, etc.
Так
само, як ви можете відобразити List[A] наList[B],
ви можете відобразити Option[A]наOption[B].
Це означає, що якщо ваш примірник Option[A] є визначеним, тобто якщо
це Some[A],
результат єSome[B],
інакше цеNone.
Якщо
ви порівняєте Option зList,None еквівалентно пустому списку :
коли ви відображуєте List[A],
ви отримуєте пустий List[B],
та коли ви відображуєте Option[A] що є None,
ви отримуєте Option[B] що єNone.
Давайте
отримаємо рік опціонального користувача:
1
valage=UserRepository.findById(1).map(_.age)// age є Some(32)
flatMap
та опції
Давайте
зробимо те ж саме зі статтю:
1
valgender=UserRepository.findById(1).map(_.gender)// gender є Option[Option[String]]
Тип
отриманого результату gender єOption[Option[String]]Чому
це?
Подумайте
про це таким чином: ви маєте контейнер Option дляUser,
тавсерединіцього контейнера ви
відображуєте примірник User наOption[String],
оскікльки це тип даих для властивості gender класуUser.
Ці
вкладені опції є надокучливими? Ось чому, так само як всі колекції, Option також провадить метод flatMap.
Точно як ви можете flatMapList[List[A]] доList[B],
ви можете зробити те саме для Option[Option[A]]:
123
valgender1=UserRepository.findById(1).flatMap(_.gender)// gender є Some("male")valgender2=UserRepository.findById(2).flatMap(_.gender)// gender є Nonevalgender3=UserRepository.findById(3).flatMap(_.gender)// gender є None
Тип
результата тепер Option[String].
Якщо користувач визначений, та його стать також визначена, ми
отримуємо згладжений Some.
Якщо або користувач, або стать не визначені, ми отримуємоNone.
Щоб
зрозуміти, як це робить, давайте подивимось, що відбувається при
пласкому відображенні списка списків рядків, завжди маючи на думці,
що Option є також колекцією, так само як List:
123456
valnames:List[List[String]]=List(List("John","Johanna","Daniel"),List(),List("Doe","Westheide"))names.map(_.map(_.toUpperCase))// результат List(List("JOHN", "JOHANNA", "DANIEL"), List(), List("DOE", "WESTHEIDE"))names.flatMap(_.map(_.toUpperCase))// результат List("JOHN", "JOHANNA", "DANIEL", "DOE", "WESTHEIDE")
Якщо
ми використовуємо flatMap,
ми відображуємо елементи внутнішніх списків, та конвертуємо все в один
плаский список рядків. Зрозуміло, нічого не залишиться від порожніх
внутрішніх списків.
Щоб
повернутись до типу Option,
уявіть, що трапиться, якщо ви відобразите список опцій рядків:
Якщо
ви тільки відобразите список обцій, результат зостанеться List[Option[String]].
З використанням flatMap,
всі елементи внутрішніх колекцій покладаються в плаский список: оидн
елемент з кожного Some[String]в
оригінальному списку розгорнуті, та покладені в результуючий список,
тоді як любе значення None в оригінальному списку не містить
жодного елементу, що має бути розгорнутий. Таким чином,
значення None ефективно відфільтровуються.
З
таким знанням, давайте подивимось знову, що робить flatMap на типі Option.
Фільтрація
опцій
Ви
можете фільтрувати опції, точно як ви фільтруєте список. Якщо
примірник Option[A] визначений, тобто якщо це Some[A],та
предикат, переданий
доfilter повертаєtrue для огорнутого значення типуA,
повертається примірникSome.
Якщо примірник Option вже None, або предикат повертаєfalse для значення в Some,
результатNone:
123
UserRepository.findById(1).filter(_.age>30)// Some(user), бо age > 30UserRepository.findById(2).filter(_.age>30)// None, оскільки age є <= 30UserRepository.findById(3).filter(_.age>30)// None, бо user вже None
For
осягнення
Тепер,
коли ми знаємо, що Option може трактуватись як колекція, та
провадить map,flatMap,filter та інші методи, що ви знаєте з
колекцій, ви, можливо, вже підозрюєте, що опції можуть бути
використані в for
осягненнях. Часто це найбільш читабельний шлях роботи з опціями,
особилво якщо ви маєте зціпити багатоmap,flatMap таfilter викликів. Якщо це тільки
поодинокий map,
це часто може бути переважним, бо це тільки трохи менш балакуче.
Якщо
ви бажаєте отримати стать для окремого користувача, ми можемо
застосувати наступну осяжність:
1234
for{user<-UserRepository.findById(1)gender<-user.gender}yieldgender// результат є Some("male")
Як
ви, можливо, знаєте з роботи зі списками, це еквівалентно до якладоних
викликів flatMap.
Якщо UserRepository вже повертає None, або Gender єNone,
результат осяжності є None.
Для користувача в цьому прикладі визначена стать, так що вона буде
повернута в Some.
Якщо
ми бажаємо отримати статі для всіх користувачів, для яких ми її вказали,
ми ітеруємо по всіх користувачах, та для кожного з них отримуємо стать,
якщо вона визначена:
Оскільки
ми ефективно сплющили відображення, результуючий тип є List[String],
та результуючий список єList("male"),
оскільки genderвизначений тільки для першого
користувача.
Використання
в лівій частині генератора
Можливо,
ви пам'ятаєте з третьої частиницієї серій, що ліва частина
генератора в осяжності є шаблоном. Це означає, що ви можете мати опції з
шаблонами в ваших осяжностях.
Ми
можемо переписати попередній приклад наступним чином:
Використання
шаблонуSome в лівій стороні генератора має ефект
видалення всіх елементів з результата колекції, для яких відповідне
значення є None.
Сціплення
опцій
Опції
також можуть бути зціплені, що є трохи подібним до зціплення часткових
функцій. Щоб зробити це, ви викликаєте orElse на примірнику Option, та передати інший
примірник Option я параметр за ім'ям. Якщо
останній є None,orElse повертає опцію, що передана йому,
інакше він повертає той, на якому він викликаний.
Гарний
приклад застосування для цього є пошук ресурсів, коли ви маєте декілька
локацій, де його можна знайти, та порядок переваг. В нашому прикладі ми
схильні шукати ресурс в каталозі конфігурації, так що ми
викликаємо orElse на ньому, передаючи
альтернативну опцію:
1234
caseclassResource(content:String)valresourceFromConfigDir:Option[Resource]=NonevalresourceFromClasspath:Option[Resource]=Some(Resource("I was found on the classpath"))valresource=resourceFromConfigDirorElseresourceFromClasspath
Це
загалом гарно підходить, якщо ви бажаєте зціпити більше ніж дві опції –
якщо ви просто бажаєте запровадити значення по замовчанню, в випадку
відсутності наданої опції, метод getOrElse може бути кращою ідеєю.
Підсумок
В
цій главі я намагався надати все, що вам треба знати про тип Option, щоб використовувати його з користю,
щоб розуміти код інших розробників Scala, та писати більш читаємий,
функціональний код. Найбільш важливий висновок, що можна отримати з
цього посту, полягає в тому, що є дуже проста базова ідея, загальна для
списків, наборів, мап, опцій, та, як ви побачите в майбутніх постах, для
інших типів даних, та існує одноманітний шлях використання ціх типів, що
разом, елегантний та потужний.
В
наступній частині цієї серії я збираюсь мати справу з ідиоматичною,
функціональною обробкою помилок в Scala.
Частина
6: Обробка помилок за допомогою Try
Коли
ми тільки граємось з новою мовою, ви можете просто пройти повз факт, що
дещо може пійти не так. Але як тільки ви захочете створити дещо
серьйозне, ви не зможете більше втекти від обробки помилок та виключень
у вашому коді. Важливість того, наскільки гарно мова підтримує вас для
роботи над помилками часто недооцінюють, з тієї чи іншої причини.
Як
з'ясовується, Scala досить гарно позиційована, коли річ доходить до
обробки помилок. В цій главі я збираюсь презентувати підхід Scala до
обробки помилок, базуючись на типі Try,
та обгрунтування цього. Я використовую можливості, введені в Scala 2.10,
та портовані назад в Scala 2.9.3, так що переконайтесь, що ваша версія
Scala в SBT є 2.9.3 або пізніша.
Підняння
та перехоплення виключень
Перед
переходом прямо до ідеоматичного підходу Scala до обробки помилок,
давайте спершу подивимось на підхід, що більше скидається на те, що
використовується для роботи з помилковими умовами, якщо ви прийшли з
мов, як Java або Ruby. Як і ці мови, Scala дозволяє вам підіймати
виключення:
1234567
caseclassCustomer(age:Int)classCigarettescaseclassUnderAgeException(message:String)extendsException(message)defbuyCigarettes(customer:Customer):Cigarettes=if(customer.age<16)throwUnderAgeException(s"Customer must be older than 16 but was ${customer.age}")elsenewCigarettes
Підняті
виключення можуть бути перехоплені, та оброблені дуже подібно до Java,
хоча і з використанням часткової функції для визначення виключень, з
якими ми бажаємо мати справи. Також, в Scalatry/catch є виразом, так що наступний код
повертає повідомлення з виключення:
1234567
valyoungCustomer=Customer(15)try{buyCigarettes(youngCustomer)"Yo, here are your cancer sticks! Happy smokin'!"}catch{caseUnderAgeException(msg)=>msg}
Обробка
помилок, функціональний шлях
Тепер,
маючи цей різновид коду обробки виключень по всьому вашому коду, може
дуже швидко стати бридким, та не іде гарно в ногу з функціональним
програмуванням. Це також досить погане рішення для застосувань з масовою
конкурентністю. Наприклад, якщо вам треба мати справу з виключенням,
викликаним в Actor, що виконується в іншому потоці, ви, очевидно, не
можете зробити це, перехоплюючи виключення – ви будете бажати можливість
отримувати повідомлення, що вказує на умови помилки.
Таким
чином, в Scala зазвичай більш бажано вказати на виникнення помилки,
повертаючи відповідне значення від вашої функції.
Не
турбуйтесь, ми не збираємось повертатись до обробки помилок в стилі C,
використовуючи коди помилок, що ми повинні перевіряти за домовленістю.
Скоріше в Scala ми використовуємо специфічні типи, що
представляють обчислення, які можуть призвести до виключень.
В
цій статті ми познайомимось з типом Try, що був введений в Scala 2.10,
та пізніше портований назад до Scala 2.9.3. Є також інший тип, Either,
який, навіть після появи Try,
все ще може бути корисним, хоча і є більш загальним.
Семантика
Try
Семантику Try краще пояснити в порівнянні з типомOption,
що був темою попередньої частини цієї серії.
Коли Option[A] є контейнером для типу A, що може бути присутнім, або ні, Try[A]представляє обчислення, що може
результувати в значенні типу A
в випадку успіху, або в деякому Throwable, якщо дещо пішло не так.
Примірники такого контейнерного типу можуть бути просто передані далі
між конкурентно обчислюваними частинами вашого застосування.
Є
два різні типи Try:
якщо екземпляр Try[A]представляє
успішне обчислення, він є примірником Success[A],
просто огортаючий значення типи A.
Якщо, з іншого боку, він представляє обчислення, де виникла помилка, це
примірник Failure[A],
огортаючи Throwable,
тобто виключення, або іншу помилку.
Якщо
ми знаємо, що обчислення може завершитись з помилкою, ми просто можемо
використати Try[A] як тип повернення для цієї функції.
Це робить можливість явною, та змушує клієнтів наших функцій вирішувати
можливість помилки, так чи інакше.
Наприклад,
давайте уявимо, що ми бажаємо написати простий завантажувач веб
сторінок. Користувач буде в змозі ввести URL веб сторінки, що треба
отримати. одна частина вашого застосування буде функцією, що розбиратиме
введений URL, та створювати java.net.URL з нього:
Як
ви можете бачити, вона повертає значення типуTry[URL].
Якщо наданий urlє синтаксично коректним, це
буде Success[URL].
Якщо, однак, конструктор URLвикликаєMalformedURLException,
це буде Failure[URL].
Щоб
досягти цього ми викорстовуємо метод-фабрику applyна об'єкті-компанйоні Try.
Цей метод очікує параметр за-ім'ям типуA(тутURL).
Для нашого прикладу це означає, що new
URL(url)виконується
зсередини методаapply об'єктаTry. В цьому методі
перехоплюються нефатальні виключення, повертаючи Failure,
що містить відповідне виключення.
Таким
чином, parseURL("http://danielwestheide.com") буде даватиSuccess[URL], що містить створений
URL, тоді якparseURL("garbage")дастьFailure[URL]з MalformedURLException.
Робота
зі значеннями Try
Робота
з примірниками Try насправді дуже подібне до роботи зі
значеннямиOption,
так що ви не побачите тут багато сюрпризів.
Ви
можете перевірити, чи Tryуспішне, викликаючи isSuccess на ньому, та потім умовно отримати
огорнуте значення, викликаючи getна ньому. Але вірте мені, не
багато ситуацій, коли вам знадобиться робити це.
Також
є можливим використати getOrElse для передачі значення по замовчанню,
якщо Try єFailure:
Якщо
наданий URL невірно сконфігурований, ми використовуємо URL DuckDuckGo як
відкат.
Ланцюжки
операцій
Одна
з найбільш важливих характеристик типу Try в тому, що, як і Option,
він підтримує всі високорівневі методи, що ви знаєте з інших типів
колекцій. Як ви побачите в наступних прикладах, це дозволяє вам
сціплювати операції зі значеннямиTry, та перехоплювати можливі
виключення, та все це в дуже прозорій манері.
Мепінг
та плаский мепінг
Мепінг
(відображення) Try[A], що єSuccess[A], доTry[B], повертає Success[B].
З іншого боку, якщо це Failure[A],
результат Try[B] буде Failure[B],
що міститиме те ж виключення, що і Failure[A]:
1234
parseURL("http://danielwestheide.com").map(_.getProtocol)// повертає Success("http")parseURL("garbage").map(_.getProtocol)// повертає Failure(java.net.MalformedURLException: no protocol: garbage)
Якщо
ви зціпите декілька операцій map,
це призведе до вкладеної структури Try,
яка, звичайно, не є тим, що вам потрібно. Розгляньте цей метод, що
повертає вхідний потік для наданого URL:
Оскільки
анонімна функції, передані до двох викликів map кожний повертає Try,
загальний тип результатаTry[Try[Try[InputStream]]].
Тут
вам допоможе факт присутності метода flatMap зTry.
Метод flatMap на Try[A] очікує функцію, що отримує A, та повертає Try[B].
Якщо наш примірник Try[A] вже Failure[A],
цей збій буде повернутий як Failure[B],
просто передаючи огорнуте виключення далі по ланцюжку. Якщо наш Try[A] є Success[A],flatMap розпаковує значенняAв ньому, та відображує його
на Try[B],
передаючи це значення до функції мепінгу.
Це
означає, що ви загалом можете створити конвеєр операцій, що потребує
значення, передані через примірникиSuccess, зціплюючи довільне число викликів
методу flatMap.
Любе виключення, що трапляється на шляху, огортається в Failure,
що значить, що кінцевий результат ланцюжка операцій також будеFailure.
Давайте
перепишемо методinputStreamForURL з попереднього прикладу, на цей час
з flatMap:
Тепер
ми отримали Try[InputStream],
що може бути Failure, що огортає виключення з любого зі
стадій, в якому він був викликаний, абоSuccess, що напряму огортає InputStream,
фінальний результат нашого ланцюжка операцій.
Фільтри
та foreach
Звичайно,
ви також можете фільтрувати Tryабо викликати foreachна ньому. Обоє роблять саме
так, як ви очікуєте після ознайомлення з Option.
Метод filter повертає Failure, якщо Try, на якоми ви його викликаєте,
вже Failure, або якщо предикот, на якому ви його
викликаєте, повертаєfalse(в якому випадку огорнуте
виключення є NoSuchElementException).
Якщо Try, на якому ви викликаєте, є Success, та предикат повертає true,
цей примірник Succcess повертається незмінним:
Функція,
передана до foreach, виконується тільки якщо TryєSuccess,
що дозволяє вам виконувати побічні ефекти. Функція, що передана
до foreach, в цьому випадку виконується
рівно один раз, будучи переданою як значення, огорнуте в Success:
Підтримка flatMap,map таfilter означає, що ви можете також
використовувати for осяжності, щоб зціпити операції
примірників Try.
Звичайно, це приводить до більш читабельного коду. Щоб продемонструвати
це, давайте реалізуємо метод, що повертає вміст веб сторінки з даним
URL, з використанням осяжностей.
Є
три місця, де речі можуть пійти не так, всі з них конвертовані
використанням типу Try.
Перше, вже реалізований метод parseURL повертає Try[URL].
Тільки якщо це Success[URL],
ми будемо намагатись відкрити з'єднання та створювати новий вхідний
потік для нього. Якщо відкриття з'єднання та створення вхідного потоку
буде успішним, ми продовжимо, нарешті отримуючи рядки веб сторінки.
Оскільки ми ефективно зціпили декілька викликів flatMap в цій осяжності, тип результату
буде пласким Try[Iterator[String]].
Будь
ласка, занотуйте, що це може бути спрощено з використанням Source#fromURL, та ще ми забули закрити наш
вхідний потік в кінці. Обоє через моє рішення тримати приклад
сфокусованим на предметі.
Порівняння
з шаблоном
В
деякій точці вашого коду ви забажаєте знати, чи примірник Try,
що ви отримали як результат деякого обчислення, представляє успіх або
ні, та виконати різні гілки коду в залежності від результату. Звичайно,
це те місце, де ви задієте порівняння з шаблоном. Це просто можливо,
оскільки обоє,Success таFailure є case класами.
Ми
бажаємо відобразити запитану сторінку, якщо вона може бути отриманою,
або надрукувати повідомлення, якщо ні:
Якщо
ви бажаєте встановити деякий різновид поведінки по замовчанню в
випадку Failure,
вам не треба використовувати getOrElse.
Альтернативою єrecover,
який очікує часткову функцію, та повертає інший Try.
Якщо recover викликаний до примірника Success,
цей примірник повертається як є. Інакше, якщо часткова функція визначена
для примірника Failure,
її результат повертається як Success.
Давайте
використаємо це, щоб надрукувати різні повідомлення, залежно від типу
огорнутого виключення:
1234567
importjava.net.MalformedURLExceptionimportjava.io.FileNotFoundExceptionvalcontent=getURLContent("garbage")recover{casee:FileNotFoundException=>Iterator("Requested page does not exist")casee:MalformedURLException=>Iterator("Please make sure to enter a valid URL")case_=>Iterator("An unexpected error has occurred. We are so sorry!")}
Тепер
ми можемо безпечно get огорнуте значення наTry[Iterator[String]], що ми присвоїли до content,
оскільки ми знаємо, що це має бути Success.
Викликcontent.get.foreach(println) буде продукувати друк на контролі Please
make sure to enter a valid URL.
Висновок
Ідеоматична
обробка помилок в Scala досить відрізняється від парадігми, відомої з
таких мов, як Java або Ruby. Тип Tryдозволяє вам інкапсулювати обчислення,
що можуть продукувати помилки, в контейнер, та зціпити операції
обчислених значень в дуже елегантний спосіб. Ви можете
застосовувати ваші відомості про колекції та значення Option
до того, як обробляти код, що може викликати помилки – все це в
одноманітний спосіб.
Щоб
підтримувати цю статтю помірної довжини я не розповідаю про всі методи,
доступні до Try.
ЯкOption,Tryпідтримує метод orElse.
Методи transform таrecoverWith також варті вашої уваги, та я
закликаю подивитись на них.
В
наступній частині ми маємо намір мат справу з Either,
альтернативним типом для представлення обчислень, що можуть призвести до
помилок, але з більшим полем зору застосування за межами обробки
помилок.
Частина
7. Тип Either
В
попередній статті я дискутував функціональну обробку помилок за
допомогою Try, що з'явився в Scala 2.10. Я також згадав існування
іншого, дещо подібного типу Either,
що є темою цієї статті. Ви вивчите, як використовувати його, коли це
робити, та які є окремі пастки.
Кажучи
про це, щонайменьше на час написання,Either має деякі суттєві вади дизайну, про
які вам треба знати, до такої міри, що дехто може
аргументувати, чи взагалі використовувати його. так чому ми взагалі
вивчаємо щодо Either взагалі?
Спершу,
люди не будуть всі мігрувати свій існуючий код для використання Tryдля
роботи з виключеннями, так що гарно бути в змозі розуміти внутрощі
цього типу, також.
Більше
того, Try не є насправді все-включено заміною
для Either,
тільки для його окремого застосування, тобто обробки виключень
функціональним шляхом. Як би не було,Tryта Either насправді доповнюють один одного,
кожний покриваючи свої випадки використання. Та, навіть з такими
недоліками, які має Either,
в окремих ситуаціях він може дуже гарно підходити.
Семантика
Подібно
до Option таTry,Either є контейнерним типом. На
відміну від вказаних типів, він приймає не один, а два параметри типу:
примірник Either[A,
B] може містити
примірник A,
або примірникB.
Це відрізняється від Tuple2[A,
B], що містить обоє, примірники A та B.
Either має рівно два підтипи,Left таRight.
Якщо об'єкт Either[A,
B]містить примірник A,
тоді Either єLeft.
Інакше він містить примірник B та єRight.
Немає
нічого в семантиці цього типу, що вказує, що один або інший підтип
представляє помилку або успіх, відповідно. Фактично, Eitherє типом загального
призначення, для використання коли ви маєте справу з ситуаціями, де
результат може бути одного з двох можливих типів. Тип не менш, обробка
помилок є популярним прикладом такого застосування, та за
домовленістю, коли використовується таким чином, Left представляє помилковий випадок, тоді
як Right містить успішне значення.
Створення
Either
Створення Either є тривіальним. Обоє, Left таRight є case класами, так що якщо
ви бажаєте реалізувати солідну можливість інтернет відносин, ви можете
зробити це наступним чином:
1234567
importscala.io.Sourceimportjava.net.URLdefgetContent(url:URL):Either[String, Source]=if(url.getHost.contains("google"))Left("Requested URL is blocked for the good of the people!")elseRight(Source.fromURL(url))
Тепер,
якщо ви викличете getContent(new
URL("http://danielwestheide.com")), ви отримаєте scala.io.Source, огорнуте в Right.
Якщшо ми передамо new
URL("https://plus.google.com"), результатом буде Left,
що містить String.
Робота
зі значеннями Either
Деяки
з дуже базових речей робиться так само, як зOption абоTry:
Ви можете запитати примірник Either , чи він isLeft абоisRight.
Ви також можете порівняти його з шаблоном, що є одним з найбільш
зручних та фамільярних шляхів роботи з об'єктами такого типу:
Ви не
можете, щонайменьше напряму, використовувати примірник Either як колекцію, шляхом, знайомим
вам з Option таTry.
Це через те, що Either розроблений бути неупередженим.
Try є success-упередженим:
він пропонує вам map,flatMap та інші методи, що всі роблять за
припущення, що TryєSuccess,
та коли це не так, вони ефективно нічого не роблять, повертаючи Failure як є.
Факт
того, що Either неупереджений, означає, що перед
тим, як ви будете робити з ним, вам треба припущення, що
це Left абоRight.
Викликаючи left абоright на значенні Either, ви отримуєте LeftProjection абоRightProjection,
відповідно, що загалом ліво- або право- обертка для Either.
Мепінг
Коли
ви маєте огортку, ви можете викликати map для неї:
123456
valcontent:Either[String, Iterator[String]]=getContent(newURL("http://danielwestheide.com")).right.map(_.getLines())// content є Right, що містить рідяки, повернуті getContentvalmoreContent:Either[String, Iterator[String]]=getContent(newURL("http://google.com")).right.map(_.getLines)// moreContent є Left, вже повернутий getContent
Без
різниці, чи Either[String,
Source]в цьому
прикладі є Left абоRight,
він буде відображений наEither[String,
Iterator[String]]. Якщо він викликаний для Right,
значення зсередини буде трансформовано. Якщо для Left,
воно буде повернуте без змін.
Звичайно,
ми можемож зробити те ж саме з LeftProjection:
123456
valcontent:Either[Iterator[String], Source]=getContent(newURL("http://danielwestheide.com")).left.map(Iterator(_))// content є Right, що містить Source, вже повернутий getContentvalmoreContent:Either[Iterator[String], Source]=getContent(newURL("http://google.com")).left.map(Iterator(_))// moreContent є Left, що містить msg, що повернув getContent в Iterator
Тепер,
якщо Either єLeft,
значення огортки трансформується, тоді як Right повернеться без змін. Так чи
інакше, результат буде типу Either[Iterator[String],
Source].
Будь
ласка, зауважте, що методmap визначений на типах проекції,
не на Either,
але він повертає значення типу Either,
не проекцію. В цьому Either відхиляється від інших контейнерних
типів, щ ови знаєте. Причиною цього було бажання
зробити Either неупередженим, але як ви
побачите, це може призвести до дуже небажаних проблем в деяких
випадках. Це також означає, що якщо ви бажаєте зціпити декілька
викликів доmap,flatMap та подібних, ви завжди маєте
запитати вашу бажану проекцію знову і знову перед кожним мепінгом.
Плаский
мепінг
Проекції
також підтримують плаский мепінг, уникаючи загальної проблеми
створення внутрішніх та зовнішніх типівEither
, чим ви скінчите, якщо ви вкладете декілька викликівmap.
Я
покладаю дуже високі надії до вашого опору зневірі, пропонуючи вам
повністю штучний приклад. Нехай ми бажаємо обчислити середнє значення
рядків в двох статтях. Ви завжди бажали зробити це, вірно? Ось як ми
можемо вирішити цю складну проблему:
Що
ми отримаємо в результаті є Either[String,
Either[String, Int]]. Тепер, коли content є вкладеною структурою Right,
ми можемо сплющити його, викликаючи метод joinRight на ньому (ви також маєте
доступний joinLeft для сплющення вкладену структуруLeft).
Однак
ми можемо уникнути створення ціх вкладених структур. Якщо ми flatMap на нашому зовнішньому RightProjection,
ми отримаємо більш приємний тип результату, розпаковуючи Right внутрішньогоEither:
Теперcontentє пласкимEither[String,
Int],, який робить його значно більше приємним для роботи,
наприклад використовувати порівняння з шаблоном.
For
осяжності
Тепер
ви, можливо, вподобали робити з осяжностями в сумісний спосіб на
різних типах даних. Ви можете зробити це, також, з проекціями Either,
але сумна правда в тому, що це вже не так мило, та є речі, що ви не в
змозі зробити, не вдаючись до бридких обхідних шляхів.
Давайте
перепишемо наш прикладflatMap,
використовуючи замість цього осяжність:
Це
не дуже погано. Зауважте, що ми маємо викликатиright на кожному Either, що ми використовуємо в наших
генераторах.
Тепер
давайте спробуємо переробити це для осяжності – оскільки вираз
yield трохи задовгий, ми бажаємо виділити деякі його частини в
визначення значень в нашій for осяжнсоті:
Це
не компілюється! Причина буде яснішою, якщо ми перевіримо, чому
відповідає ця осяжність, якщо ми приберемо цукор. Це транслюється в
дещо, подібне наступному, хоча менш читабельне:
Проблема
в тому, що включачи визначення значень в нашу осяжність, автоматично
вводиться новий виклик до map – на результаті
попереднього виклику до map,
що повернув Either,
неRightProjection.
Як ви знаєте, Either не визначає метод map,
що трохи дратує компілятор.
Ось
де Either показує свою огидну гримасу.
В цьому прикладі визначення значень не є суто необхідними. Якщо це
буде так, ви можете піти кружним шляхом, замінюючи кожне визначення
значення на генератор, от так:
Важливо
бути попередженим про цю слабкість дизайну. Це не робить Either непридатним, але може
призвести до серйозних головних білей, якщо ви не маєте уяви, що
відбувається.
Інші
методи
Проективні
типи мають деякі інші корисні методи:
Ви
можете перетворити ваш примірник Either
наOption , викликаючиtoOptionна одній з його проекцій.
Наприклад, якщо ви маєте eтипуEither[A,
B],e.right.toOption повернеOption[B].
Якщо ваш примірник Either[A,
B] є Right,
тоді Option[B] будеSome.
Якщо це Left,
це будеNone.
Звичайно, зворотня поведінка може бути досягнута, коли
викликаєтся toOption наLeftProjection вашого Either[A,
B]. Якщо вам треба послідовність з любого одного значення,
або нічого, замість цього використовуйтеtoSeq.
Складання
(фолдінг)
Якщо
ви бажаєте трансформувати значення Either незалежно від того, це Left абоRight,
ви можете зробити це силами методу fold, що визначений на Either,
що очікує дві функції тарнсформації з однаковим типом результата, одну
для того, щоб викликатись, коли Either єLeft,
другу - коли Right.
Щоб
продемонструвати це, давайте скомбінуємо дві операції мепінгу, що ми
реалізували на LeftProjectionтаRightProjection вище:
В
цьому прикладі ми перетворили наші Either[String,
Source] на Iterator[String],
не важливо, це Left абоRight.
Ви можете точно так же повернути новий Either знову, або виконати побічний
ефект, та повернути Unit з двох функцій. Як такий,
викликfold провадить гарну альтернативу до
порівняння шаблонів.
Коли
використовувати Either
Тепер,
коли ми побачили, як робити зі значеннями Either, та що ви маєте пам'ятати,
давайте перейдемо до деяких специфічних випадків використання.
Обробка
помилок
Ви можетевикористовувати Either для обробки виключень, дуже подібно
до Try.Eitherмає
одну перевагу над Try:
ви можете мати більш специфічні типи помилок під час компіляції, тоді
як Try весь час використовуєThrowable. Це означає, що Eitherможе бути гарним вибором для
очікуваних помилок.
Вам
треба реалізувати метод, на кшталт такого, делегуючи до дуже корисного
о'бєкту Exception з пакункуscala.util.control:
Причиною,
чому ви бажаєте зробити це - тому що методи, що провадить scala.util.Exception, дозволяють вам перехоплювати тільки
певні типи виключень, та результуючий тип часу компіляції завжди
буде Throwable.
Маючи
такий метод, ви можете передати дале очікувані виключення в Either:
Ви
будете мати інші очікувані умови обробки, та не всі вони закінчаться в
коді третіх сторін, що підіймає виключення для обробки, як в прикладі
вище. В ціх випадках, немає реальної потреби самому підіймати
виключення, тільки для перехоплення його, та огорнути його в Left.
Замість цього просто визначте ваш власний тип помилки, бажано як case
клас, та поверніть Left, огортаючи примірник цієї помилки.
Ви
повинні уникати використання Either для огортання неочікуваних виключень.
Tryробить це краще, без усіх
слабкостей, з якими ви стикаєтесь, коли має справу зEither.
Обробка
колекцій
Загалом, Either досить гарно підходить, якщо ви
бажаєте обробляти колекції, де для деяких елементів в
цій колекції це може призводити до проблематичної умови, але не
повинно напряму призводити до виключення, що буде призводити до
переривання обробки залишку колекції.
Давайте
уявимо, що для нашої індустріальної системи веб відношень, ми
використовуємо деякий різновид чорного списку:
BlackListedResource представляє URL заблокованих
веб сторінки, плюс список людей, хто спробував навідати цю сторінку.
Тепер
ми бажаємо обробити цей чорний список, де наша головні ціль
ідентифікувати проблематичних громадян, тобто тих, хто намагався
навідувати заблоковані сторінки. В той же час ми бажаємо
ідентифікувати підозрілі веб сторінки – якщо один громаданин намагався
навідатись на заблоковану сторінку, ми повинні вважати, що наші
підозрювані якось оминають на фільтр, та нам треба розслідувати це.
Нам
треба створити послідовність зі значень Either,
з примірниками Left, що представляють підозрілі URL,
та Right, що містять набори проблемним
громадянам. Це робить майже легким ідентифікувати обоє, наших
проблемних громадян, та підозрілі веб сторінки:
Ці,
більш загальні, приклади використання за межами обробки виключень, це
те, де Either дійсно сяє.
Висновок
Ви
навчились, як використовувати Either,
які тут є пастки, та потім покладати його до використання в вашому
коді. Цей тип не позбавлений слабкостей, та чи ви бажаєте мати з ним
справу, та вбудовувати в свій код, покладається виключно на вас.
На
практиці, як ви побачите, тепер, коли ви маєте Try,
для нього не буде дуже багато застосувань. Тим не менш, добре знати
про нього, з двох причин: в ситуаціях, де він може бути бажаним
інструментом, та щоб розуміти код pre-2.10 Scala, де він
використовується для обробки помилок.
Частина
8: Ласково просимо до майбутнього
Як
зацікавлений та заповзятий Scala розробник, ви вже, напевне, чули
про підхід Scala до стправ конкурентності – або можливо, саме це
захопило вас в першу чергу. Вказаний підхід має обгрунтування щодо
конкурентності, та написання гарно поводячих себе, конкурентних
програм значно простіше, ніж за використання низькорівневих API
конкуренції, яким ви протистояли в більшості інших мов.
Один
з наріжних каменів цього підходу є Future,
іншим є Actor.
Останній буде предметом окремої глави. Я буду пояснювати що гарного,
та як ви можете викорстовувати їх в функціональний спосіб.
Будь
ласка, переконайтесь, що ви маєте версію 2.9.3 або пізнішу, якщо ви
бажаєте залучити свої руки до справи, та спробувати приклади
власноруч. Можливості, що ми дискутуємо тут, були вбудовані в ядро
Scala з дистрибутива релізу 2.10.0, та потім зворотньо портовані до
Scala 2.9.3. В оригіналі, з дещо іншим API, вони були частиною
конкурентного тулкиту Akka.
Чому
послідовний код може стати поганим
Уявімо,
ми бажаємо приготувати капучіно. Ви можете просто виконати наступні
кроки, один за одним:
Змоліть
потрібні зерна кави
Підігрійте
декілька води
Зваріть
еспресо, використовуючи змолену каву та підігріту воду
Зпінте
деяке молоко
Змішайте
еспресо та зпінене молоко, щоб отримати капучіно
Перекладаючи
на код Scala, ви маєте зробити щось таке:
1234567891011121314151617181920212223242526272829
importscala.util.Try// Деякі псевдоними типів, тільки для отримання більш осмислених сигнатур методів:typeCoffeeBeans=StringtypeGroundCoffee=StringcaseclassWater(temperature:Int)typeMilk=StringtypeFrothedMilk=StringtypeEspresso=StringtypeCappuccino=String// умовна реалізація окремих кроків:defgrind(beans:CoffeeBeans):GroundCoffee=s"ground coffee of $beans"defheatWater(water:Water):Water=water.copy(temperature=85)deffrothMilk(milk:Milk):FrothedMilk=s"frothed $milk"defbrew(coffee:GroundCoffee,heatedWater:Water):Espresso="espresso"defcombine(espresso:Espresso,frothedMilk:FrothedMilk):Cappuccino="cappuccino"// деякі виключення для речей, що можуть пійти не так на окремих кроках// (нам знадобляться деякі пизніше, використовуйте інші коли експирементуємо// з кодом):caseclassGrindingException(msg:String)extendsException(msg)caseclassFrothingException(msg:String)extendsException(msg)caseclassWaterBoilingException(msg:String)extendsException(msg)caseclassBrewingException(msg:String)extendsException(msg)// проходимо цей код послідовно:defprepareCappuccino():Try[Cappuccino]=for{ground<-Try(grind("arabica beans"))water<-Try(heatWater(Water(25)))espresso<-Try(brew(ground,water))foam<-Try(frothMilk("milk"))}yieldcombine(espresso,foam)
Робити
так має декілька переваг: ви отримуєте дуже читабельні
крок-за-кроком інструкції, що робити. Більше того, ви, скоріше, не
будете збентажені при приготуванні капучіно в цей шлях, бо ви
уникаєте перемикання контексту.
З
другого боку, приготування вашого капучіно в такій манері
крок-за-кроком означає, що ваш мозок та ваше тіло буде простоювати
на довгих відтинках часу під час цього процесу. Доки ви ждете
змолювання кави, ви ефективно заблоковані. Тільки коли це
скінчиться, ви будете в змозі підігрівати воду, і так далі.
Ясно,
що це розпорошення цінних ресурсів. Дуже можливо, що ви б хотіли
розпочати декілька кроків, та виконувати їх одночасно. Коли ви
побачили, що вода та змолювання кави готове, ви починаєте варити
еспресо, та при цьому починаючи процес зпінення молока.
Це,
насправді, так само, коли ви пишете якійсь код. Веб сервер має дуже
багато потоків для обробки запитів, та створення відповідних
відповідей. Ви не бажаєте блокувати ці ціння потоки, очікуючи
результати запиту до бази даних, або для виклику іншого HTTP
сервіса. Замість цього, ви бажаєте асинхронну модель програмування,
та неблокуючий IO, так що під час обробки запиту, що чекає на
відповідь від бази даних, потік веб сервера, що обробляє цей запит,
може обробляти потреби деякого іншого запиту, замість простоювати в
стороні.
“Я
чув ви любите зворотні виклики, так що я поставив зворотній виклик
в ваш зворотній виклик!”
Звичайно,
ви знаєте все це - що за допомогою Node.js наразі стало предметом
несамовитості серед крутих дітлахів. Підхід, використаний в Node.js
та деяких інших є комунікація через зворотні виклики, виключно. На
жаль, це може дуже просто призвести до розростання безладу в
викликах викликів через виклики, що зробить ваш код складним для
читання та налаштування.
ScalaFuture дозволяє зворотні виклики, як ви
скоро побачите, але вона провадить значно кращі альтернативи, так
що вам вони, напевне, не дуже знадобляться.
“Я
знаю Futures, і вони повністю даремні!”
Ви
можете бути також знайомим з іншими реалізаціями Future, більш замітною з яких є
та, що провадиться в Java. Насправді вам нема чого багато робити з
ціма Java future, окрім перевіряти, ци вона завершилась, або просто
блокуватись, доки вона завершиться. Коротко кажучи, вони майже
даремні, та напевне не додають радощів при роботі.
Якщо
ви думаєте що майбутнє Scala щось подібне до цього, приготуйтесь до
сюрпризу. Починаємо!
Семантика
Future
ScalaFuture[T]
розташоване в пакунку scala.concurrent,
є контейнерним типом, що представляє обчислення, що, як
очікуєтсья, при
нагоді завершиться
значенням типу T.
Гаразд, обчислення може пійти не так, або вийти в таймаут, так що
коли майбутне завершиться, воно може взагалі не бути успішним в
кінці кінців, і тоді воно міститиме виключення.
Future є одноразовим контейнером –
коли майбутнє завершене, воно ефективно незмінне. Також тип Future провадить інтерфейс тільки
для читанняобчисленого значення.
Завдання записуthe обчисленного значення
досягається черезPromise.
Таким чином, є ясна сепарація між сферами в дизайні API. В цьому
пості ми сфокусуємось на попередньому, відклавши використання типуPromise до наступної статті в цій серії.
Робота
з Future
Є
декілька шляхів, як ви можете робити з майбутнім в Scala, що ми
збираємось перевірити, переписавши наш приклад с капучіно, щоб
задіяти типFuture.
З початку, нам треба переписати всі функції, що можуть бути виконані
конкурентно, так, щоб вони могли безпосередньо повертали Future, замість обчислення своїх
результатів в блокуючий спосіб:
importscala.concurrent.futureimportscala.concurrent.Futureimportscala.concurrent.ExecutionContext.Implicits.globalimportscala.concurrent.duration._importscala.util.Randomdefgrind(beans:CoffeeBeans):Future[GroundCoffee]=Future{println("start grinding...")Thread.sleep(Random.nextInt(2000))if(beans=="baked beans")throwGrindingException("are you joking?")println("finished grinding...")s"ground coffee of $beans"}defheatWater(water:Water):Future[Water]=Future{println("heating the water now")Thread.sleep(Random.nextInt(2000))println("hot, it's hot!")water.copy(temperature=85)}deffrothMilk(milk:Milk):Future[FrothedMilk]=Future{println("milk frothing system engaged!")Thread.sleep(Random.nextInt(2000))println("shutting down milk frothing system")s"frothed $milk"}defbrew(coffee:GroundCoffee,heatedWater:Water):Future[Espresso]=Future{println("happy brewing :)")Thread.sleep(Random.nextInt(2000))println("it's brewed!")"espresso"}
Тут
є декілька речей, що потребує пояснення.
Зпершу,
є метод apply об'єкта-компанйона Future,
що потребує двох аргументів:
Обчислення,
що виконується асинхронно, передається як параметр по імені body. Другий аргумент, в
окремому списку аргументів, є неявним,
що означає, що ми не маємо вказувати його, якщо співпадаюче неявне
значення визначене десь в полі зору. Ми гарантуємо, що це наш
випадок, імпортуючи глобальний контекст виконання.
ExecutionContext - це щось, що може виконувати наше
майбутнє, та ви можете думати про це, як про пул потоків.
Оскільки ExecutionContext доступний неявно, ми маємо
тільки одноелементний список аргументів. Списки з одного елементу
можуть бути заточені в фігурні дужки, замість звичайних. Люди часто
використовують це, коли викликають методfuture,
після чого воно виглядає якби ми використовували можливість мови, а
не викликаємо звичайний метод. ExecutionContext є неявним параметром для
віртуально всіхFutureAPI.
Більше
того, звичайно, в цьому прикладі ми насправді не обчислюємо нічого,
тому ми покладаємо деяке випадкове очікування, просто для цілей
демонстрації. Ми також друкуємо в консоль перед та після наших
“обчислень”, щоб зробити невизначену та конкурентну природу нашого
кода яснішою.
Обчислення
значення, що повертається Future, буде починатись в деякий
невизначений час після створення Future, коли деякий потік буде присвоєне
йому з ExecutionContext.
Зворотні
виклики
Іноді,
коли речі прості, використання зворотнього виклику може бути
повністю гарним. Зворотні виклики для майбутнього є частковими
функціями. Ви можете передавати зворотній виклик до методаonSuccess.
Він буде викликаний, тільки якщо Future обчислиться успішно, та
якщо так, він отримає обчислене значення в якості входу:
123
grind("arabica beans").onSuccess{caseground=>println("okay, got my ground coffee")}
Подібним
чином, ви можете зареєструвати зворотній виклик по збою, за
допомогою методаonFailure.
Ваш зворотній виклик буде отримувати Throwable,
але він буде викликаний, тільки якщоFuture не завершиться успішно.
Зазвичай
краще комбінувати ці двоє, та реєструвати зворотній виклик
завершення, що буде обробляти обоє випадки. Вхідний параметр для
зворотнього виклику є Try:
12345
importscala.util.{Success,Failure}grind("baked beans").onComplete{caseSuccess(ground)=>println(s"got my $ground")caseFailure(ex)=>println("This grinder needs a replacement, seriously!")}
Оскільки
ми передаємо жарені зерна, виключення виникає в методі grind,
що призведе до завершення Future зFailure.
Компонування
майбутнього
Використання
зворотніх викликів може бути досить болючим, якщо ви маєте вкладені
зворотні виклики. На щастя, ви не маєте робити це! Реальна міць
майбутнього Scala в тому, що їх можна компонувати.
Якщо
ви слідували цій сериї, ви напевне помітили, що всі ваші контейнерні
типи, що ми дискутували, роблять можливим відображення їх, пласке
відображення, або використання іх в осяжностях. Та якщо я кажу,
що Future також є контейнерним типом, це
означає, що тип ScalaFuture дозволяє вам робити все те ж саме,
і це, взагалі, не є сюрпризом.
Реальне
запитання наступне: що це насправді означає, виконувати ці операції
до того, що навіть досі не завершилось?
Відображення
майбутнього
Чи
не баажли ви подорожувати в часі, та бути тим, хто встановлює мапу
майбутнього? Як розробник Scala, ви можете робити саме це! Уявіть,
що коли вода зігрілась, ви бажаєте перевірити, чи температура в
нормі. Ви можете зробити це, відображуючи Future[Water] наFuture[Boolean]:
1234
valtemperatureOkay:Future[Boolean]=heatWater(Water(25)).map{water=>println("we're in the future!")(80to85).contains(water.temperature)}
Future[Boolean] присвоєне доtemperatureOkay, буде з часом містити успішно
обчислене значення. Змініть реалізацію heatWater, так що коли вона викликає
виключення (можливо через те, що ваш нагрівач вибухнув, або
щось інше) , та дивіться, як we're
in the futureне
буде ніколи надрукованим в консолі.
Коли
ви пишете функцію, що ви передаєте до map,
ви в майбутньому, або скоріше в можливому майбутньому. Ця функція
відображення виконуєтья так швидко, як тільки ваш
примірник Future[Water] був завершений успішно.
Однак, плин часу, в якому це трапляється, може бути не тим, в якому
живете ви. Якщо ваш примірник Future[Water] схибить, те, що
відбувається в функції, що ви передаєте в map, ніколи не відбудеться.
Замість цього, результат викликуmapбудеFuture[Boolean], що міститьFailure.
Утримування
майбутнього пласким
Якщо
обчислення одного Future залежить від результату іншого,
ви, напевне, бажаєте вдатися до flatMap щоб виключити глибоко вкладеної
структури для майбутніх.
Наприклад,
давайте уявімо, що процес насправді виміряє температуру за деякий
час, так що ви бажаєте визначати годніть температури також
асинхронно. Ви маєте функцію, що приймає інтерфейс Water та повертає Future[Boolean]:
І
знову, функція відображення виконується після (якщо взагалі)
примірник Future[Water]
був завершений успішно, як надіємось, з допустимою температурою.
For
осяжності
Замість
виклику flatMap,
ви будете звичайно писати for осяжності, що в основному те ж саме,
але краще виглядає. наш приклад вище може бути переписаний таким
чином:
Якщо
ви маєте декілька обчислень, що можуть бути обчислені паралельно,
вам треба потурбуватись, щоб ви створили відповідні примірники Futureза
межами for осяжності.
Це
гарно читається, але оскільки for осяжність лише інше представлення
для вкладених викликівflatMap,
це означає, що Future[Water], створений в heatWater є тільки насправді
уособленим після того, як Future[GroundCoffee] був обчислений успішно. Ви
можете перевірити це, поглянувши на послідовний вивід на консоль, що
іде від функцій, що ми тільки но реалізовали.
Таким
чином переконайтесь, що створили всі незалежні майбутні перед
осяжністю:
Тепер
ми створили три майбутніх перед for осяжністю, що стартують будучи
безпосередьно завершеними, та виконуються конкурентно. Якщо ви
подивитесь на вивід в консолі, ви побачите, що вивід
недетермінований. Єдина річ, що певна, це те, що вивід"happy
brewing" буде останнім.
Оскільки метод, що викликається, потребує значень, що походять від
інших двох майбутніх, він створюється в нашій for осяжності,
тобто після того, як ті майбутні завершені успішно.
Проекції
збоїв
Ви
маєте бути попереджені, щоFuture[T] є схильним до успіху,
дозволяючи вам використовувати map,flatMap,filteretc. за припущення, що
він завершується успішно. Іноді, ви можете побажати зробити це, в
цей функціональний спосіб для плину часу, де речі ідуть невірно.
Викликаючи метод failed
на примірникуFuture[T],
ви отримаєте проекцію збою, що є Future[Throwable].
Тепер ви можете, наприклад, застосувати map
до цього Future[Throwable],
та ця функція відображення буде виконуватись, якщо
оригінальний Future[T] був завершений зі збоєм.
Огляд
Ви
побачилиFuture,
та це виглядає блискуче! Факт, що це тільки інший контейнерний тип,
що може бути скомпонований та використаний в функціональний спосіб,
робить роботу з ним дуже приємною.
Перетворення
блокуючого кода на конкурентний може бути досить простим, огортаючи
його в виклик доfuture.
Однак, краще краще для початку бути неблокуючим. Щоб досягти цього,
ви маєте зробити Promise для завершення Future.
Це, та використання майбутнього на практиці буде темою наступної
частини в цій серії.
Розділ
9: Promise та Futures на практиці
В
попередньому розділі серії я ввів тип Future,
його підлеглу парадігму, та як використовувати його для написання
високо читабельного та компоновного кода з асинхронним виконанням.
В
цій главі я також споминав Future як дійсно одну частину пазла:
це тип тільки для читання, що дозволяє вам робити зі значеннями,
та обробляти відмови, та робити це в елегантний спосіб. Однак, щоб
ви могли читати обчислене значення з Future,
має бути шлях для деякох іншої частини нашої програми, щоб
покласти туди це значення. В цьому пості я покажу вам, як це
робиться за допомогою типу Promise,
за чим послідують деякі інструкції, щодо того, як використовувати
майбутні та обіцянки на практиці.
Обіцянки
В
попередній главі про майбутнє ми мали послідовний блок коду, що ми
передавали до методу apply об'єкта-компанйона Future,
та, маючи ExecutionContext в полі зору, він магічно
виконував цей блок асинхронно, повертаючи результат якFuture.
Хоча
це простий шлях отримати Future, коли воно вам треба, є
альтернативний шлях для створення примірниківFuture,
та мати завершеними, з успіхом або збоєм. ТутFuture провадить інтерфейс виключно
для запитів,Promise є дружнім типом, що дозволяє вам
завершити Future, покладаючи значення до нього.
Це робиться рівно один раз. Коли Promiseзавершений, його вже
неможливо змінити.
Примірник Promise завжди пов'язаний рівно до
одного примірника Future.
Якщо ви викличете метод apply дляFuture знову в REPL, ви,
напевно помітите, що Future поверне такожPromise:
Об'єкт,
що ви отримаєте назад, буде DefaultPromise,
що реалізує обоє, Future таPromise.
Однак, це лише деталь реалізації. Future таPromise, до якого він належить, може
дуже добре бути окремими об'єктами.
Що
показує цей малий приклад, це що немає очевидного шляху завершитиFuture, інакше ніж через Promise– метод apply наFuture є тільки милою
функцією-допоміжником, що захищає вас від цього.
Тепер
давайте подивимось, як ви можете докласти свої руки, та напряму
поробити з типом Promise.
Обіцянка
рожевого майбутнього
Коли
ми кажемо про обіцянки, вони можуть справдитись, або ні. Очевидні
приклади є політика, вибори, внески під час компанії, та подальша
законотворчість.
Уявімо
політикана, що під час обрання до кабінета обіцяв виборцям
зниження податків. Це можна представити як Promise[TaxCut],
що ви можете створити, викликаючи метод apply на об'єкті-компанйона Promise, таким чином:
123456
importconcurrent.PromisecaseclassTaxCut(reduction:Int)// або дає тип як параметр типу методу-фабрики:valtaxcut=Promise[TaxCut]()// або дає підказку компілятору, вказуючи тип вашого val:valtaxcut2:Promise[TaxCut]=Promise()
Коли
ви створили Promise,
ви можете отримати Future, що належить йому, викликаючи
метод future на екземпляріPromise:
1
valtaxcutF:Future[TaxCut]=taxcut.future
Повернений Future не не бути тим самим об'єктом,
що і Promise,
але виклики метода futureнаPromise декілька разів буде однозначно
повертати той же об'єкт, так що є відношення один-до-одного
між Promise таFuture зберігаєтся.
Завершення
Promise
Як
тільки ви створили Promise, та повідомили світові, що ви
будете доставляти до нього в очікуваномуFuture,
ви зробили все можливе, щоб це трапилось.
В
Scala ви можете завершити Promise або успіхом, або невдачею.
Доставка
до вашого Promise
Щоб
завершити Promise успішно, ви викликаєте його
методsuccess,
передаючи йому значення, щоFuture асоціює з тим, що має бути:
1
taxcut.success(TaxCut(20))
Коли
ви це зробили, цей примірник Promise більше не записується, та
майбутні спроби зробити це призведуть до виключення.
Також
завершення вашого Promise таким чином призведе до
успішного завершення асоційованого Future.
Тепер будуть викликані любі обробники успіху або завершення на
майбутньому, або, наприклад, якщо ви відображуєте майбутнє,
функція відображення буде викликана саме тепер.
Звичайно
завершення Promise та обробка завершеного Future не буде відбуватись в одному
потоці. Більш вірогідно, що ви створите свій Promise,
розпочнете обчислення його значення в іншому потоці, та
безпосередньо повернете незавершений Future викликаючому.
Щоб
проілюструвати це, давайте зробимо щось на кшталт цього в обіцянці
зниження податків:
123456789101112
objectGovernment{defredeemCampaignPledge():Future[TaxCut]={valp=Promise[TaxCut]()Future{println("Починаємо наступний період каденції")Thread.sleep(2000)p.success(TaxCut(20))println("Ми зменшили податки! Ви маєте переобрати нас!!!!1111")}p.future}}
Будь
ласка, не засмучуйтесь через використання метода apply до об'єкта-комапанйона Futureв цьому прикладі. Я просто
використовую його, оскільки це так зручно для виклику блоку коду
асинхронно. Я можу так само реалізувати обчислення результату (що
включає багато сну) вRunnable, що виконуєтья асинхронно вExecutorService,
з багато більшим шаблонним кодом. Смисл в тому, що Promise не завершений у викликаючому
потоці.
Тепер
давайте спокутувати наші обіцянки кампанії, та додамо
функцію-зворотній викликonComplete до нашого майбутнього:
123456789
importscala.util.{Success,Failure}valtaxCutF:Future[TaxCut]=Government.redeemCampaignPledge()println("Тепер, коли вони обрані, перевіримо, як вони пам'ятають обіцянки...")taxCutF.onComplete{caseSuccess(TaxCut(reduction))=>println(s"Диво! Вони дійсно зменшили податки на $reduction відсотків!")caseFailure(ex)=>println(s"Вони не дотримались обіцяного! Знову! Тому що ${ex.getMessage}")}
Якщо
ви спробуєте це декілька разів, ви побачите, що порядок друку на
консоль не визначений. Іноді обробник буде викликаний, та підпаде
під випадок успіху.
Порушення
обіцянок по-джентельменськи
Як
политикан, ви напевне не раз порушували свої обіцянки. Як
розробник Scala, ви часом не маєте іншого вибору, так чи інше.
Якщо це трапляється, ви все ше можете завершити ваш
екземпляр Promise тактично, викликавши його
методfailure, та передати йому виключення:
12345678910111213
caseclassLameExcuse(msg:String)extendsException(msg)objectGovernment{defredeemCampaignPledge():Future[TaxCut]={valp=Promise[TaxCut]()Future{println("Починається новий період каденції")Thread.sleep(2000)p.failure(LameExcuse("глобальна економічна криза"))println("Ми порушили обіцянку, але нас зрозуміють")}p.future}}
Це
реалізація методаredeemCampaignPledge() підходить до багатьої порушених
обіцянок. Коли ви завершили Promise методомfailure,
він більше не записуєтсья, так само, як випадку методаsuccess.
Асоційований Future тепер завершиться з Failure,
також, так що функція зворотнього виклику вище виконає випадок
невдачі.
Якщо
ви вже маєте Try,
ви також можете завершити Promise викликом метода complete.
Якщо Try єSuccess,
асоційований Futureбуде
завершений успішно, зі значенням всередині Success.
Якщо цеFailure,Future завершиться невдачею.
Базова
на Future програмування на практиці
Якщо
ви бажаєте використати парадігму на базі майбутнього, щоб
покращити маштабованість вашого застосування, ви маєте розробити
ваше застосування як неблокуюче від самого початку, що, загалом,
означає, що функції на всіх рівнях у всьому вашому застосуванні є
асинхронними, та повертають майбутнє.
Вірогідний
випадок на сьогодні є розробка веб застосування. Якщо ви
використовуєте сучасний веб фреймворк Scala, він дозволить вам
повертати ваші відповіді як щось на кшталт Future[Response], замість блокування, та потім
повертання вашого завершеного Response.
Це важливо, оскількі це дозволяє вашому веб серверу оброляти
величезну кількість відкритих з'єднань за допомогою порівняно
малої кількості потоків. Коли ви весь час надаєте вашому
серверу Future[Response],
ви максимізуєте використання виділеного пулу потоків вашого
сервера.
В
кінці кінців, сервіс в вашому застосуванні може зробити декілька
викликів до рівня вашої бази даних, та/або деякий зовнішній веб
сервіс, отримуюючи декілька майбутніх, та потім скомпонувати їх
результат в один загальнийFuture,
все в дуже читальній for осяжності, як ви бачили в попередній
главі. Веб прошарок буде перетворювати цей Future в Future[Response].
Однак
як це реалізовати на практиці? Є три випадкі, які ви маєте
розглянути:
Неблокуючий
IO
Ваше
застосування буде, найбільш вірогідно, включати багато IO.
Наприклад, веше веб застосування буде звертатись до бази даних, та
воно може діяти як клієнт, що викликає інші веб сервіси.
Якщо
це загалом можливо, використовуйте бібліотеки, що базуються на
неблокуючому Java IO, або використовуючи Java NIO API напряму, або
через бібліотеки, як Netty. Такі бібліотеки, також, можуть
обслуговувати багато з'єднань з пулом потоків помірного
розміру.
Розробка
такої бібліотеки самому є одним з декількох місць, де пряма робота
з Promise
має багато сенсу.
Блокуючий
IO
Іноді
немає доступної неблокуючої бібліотеки NIO. Наприклад, більшість
драйверів баз даних, що ви знайдете в світі Java, наразі
використовують блокуючий IO. Якщо ви зробили запит до вашої бази
даних за допомогою такого драйверу, щоб відповісти на HTTP запит,
цей виклик буде зроблений на потоці веб сервера. Щоб цникнути
цього, розмістіть весь код, що розмовляє з базо даних, в
блок future,
таким чином:
1234
// отримаємо назад Future[ResultSet] або щось подібне:Future{queryDB(query)}
До
тепер ми завжди використовували неявний доступний глобальний ExecutionContextдля
виконання блоків майбутнього. Можливо буде гарною ідеєю створити
виділений ExecutionContext, що ви будете мати в полі зору
на рівні вашої бази даних.
Ви
можете створити ExecutionContext з JavaExecutorService,
що означає, що ви будете в взмозі підлаштувати пул потоків для
виконання ваших викликів до бази даних асинхронно, незалежно від
решти вашого застосування:
В
залежності від природи вашого застосування, час від часу буде
виникати потреба викликати довготривалі завдання, що зовсім не
мають жодного IO, що означає, що вони прикуті до CPU. Вони також
не мають виконуватись в потоці веб сервера. Таким чином, вам треба
також перетворити їх на Futures:
123
Future{longRunningComputation(data,moreData)}
Ще
раз, якщо ви маєте довготривале обчислення, що прив'язане до
процесора, буде гарною ідеєю виконання их в окремому ExecutionContext.
Як перетворити ваші різноманітні пули потоків дуже залежить від
вашого окремого застосування, та за рамками цієї глави.
Підсумок
В
цій главі ми розглянули обіцянки, записуючу частину парадігми
конкурентності на основі майбутнього, та як використовувати їх для
завершення Future.
За чим послідовали деякі поради щодо використання ціх можливостей
на практиці.
І
наступнй частині цієї серії ми зробимо крок наза, від проблем
конкурентності, та розглянемо, як функціонально програмування в
Scala може допомогти вам зробити ваш код більш повторно уживаним,
твердження, що довгий час асоціювали з об'єктно-орієнтованим
програмуванням.
Частина
10: Залишаємось DRY з функціями вищого порядку
В
попередніх розділах я обсудив композитну природу контейнерних
типів Scala. Як з'ясовується, можливість компонуватись є
властивістю, що ви знайдете не тільки вFuture,Try,
та інших контейнерних типах, але також в функціях, що є
першокласними громадянами в мові Scala.
Композитність
природно призводить повторним виконанням. Хоча остання часто
проголошується однією з найбільших переваг
об'єктно-орієнтованого програмування, це риса, що безумовно
притаманна чистим функціям, тобто функціям, що не мають побічних
ефектів та референтно прозорі.
Очевидний
шлях є реалізувати нову функцію, викликавши вже існуючі функції
в її тілі. Однак є інші шляхи повторного використання існуючих
функцій: в цьому блог пості я продискусую деякі основи
функціонального програмування, яких ми уникали до тепер. Ви
вивчите, як слідувати принципам DRY,
підваживши функції вищого порядку, щоб використовувати існуючий
код в нових контекстах.
Щодо
функцій вищого порядку
Функція
вищого порядку, на відміну від функцій першого порядку, може
мати одну з трьох форм:
Один
або більше їх параметрів є функцією, та вона повертає деяке
значення.
Вона
повертає функцію, але жодний з її параметрів не функція.
Обоє
з переліченого: один або більше з її параметрів є функція, та
вона повертає функцію.
Якщо
ви слідували цій серії, ви бачили багато використань фукнцій
вищого порядка першого типу: ми викликали методи, як map,filter,
або flatMap, та передавали туда функцію,
що використовувалась для трансформації або фільтрування
колекції деяким чином. Дуже часто функції, що ми
передаємо до ціх методів, були анонимними функціями, іноді з
деякими елементами дублікації.
В
цій главі ми будемо розглядати тільки те, що для нас можуть
зробити функції двох інших типів: перший з них дозволяє
продукувати нові функції, базуючись на деяких вхідних даних,
тоді як інший дає нам потужність та гнучкість компонувати нові
функції, що деяким чином базуються на існуючих функціях. В обох
випадках ми можемо уникнути дублікацію коду.
Та
з нічого була народжена функція
Ви
можете думати, що можливість створювати нові функції, базуючись
на деяких вхідних даних, не є конче корисною. Хоча ми бажаємо
мати справу як скомпонувати нові функції на основі, давайте
спочатку поглянемо, як можна використати функцію, що продукує
нові функції.
Давайте
уявимо, що ми реалізуємо поштовий сервіс, де користувачі мають
змогу конфігурувати, коли пошта має бути блокована. Ми
представляємо листи як примірники простого кейс класа:
Ми
бажаємо бути в змозі фільтрувати нові листи за критерієм,
заданим користувачем, так що ми маємо фільтруючу функцію, що
використовує предикат, функцію типуEmail
=> Boolean, щоб
визначити, чи має лист бути блокований. Якщо предитак є
true,
лист приймається, інакше він буде блокований:
Зауважте,
що використовуючи псевдоним типу для нашої функції, ми можемо
робити з більш змістовними іменами в нашому коді.
Тепер,
щоб дозволити користувачеві конфігурувати поштовий фільтр, ми
можемо реалізувати деякі функції-фабрики, що продукують функціїEmailFilter, сконфігуровані для уподобань
користувача:
Кожна
з цьох чотирьох vals є функція, що повертає EmailFilter,
перші дві приймають на входіSet[String], що представляє надсилачів,
ініші дві - Int, що представляє довжину
тіла листа.
Ми
можемо використовувати кожну з ціх функцій для створення новоїEmailFilter, що ми можемо передати до
функціїnewMailsForUser:
1234567
valemailFilter:EmailFilter=notSentByAnyOf(Set("johndoe@example.com"))valmails=Email(subject="It's me again, your stalker friend!",text="Hello my friend! How are you?",sender="johndoe@example.com",recipient="me@example.com")::NilnewMailsForUser(mails,emailFilter)// returns an empty list
Цей
фільтр видаляє один лист зі списку, оскільки наш користувач
вирішив покласти надсилача в чорний список. Ми можемо
використати наші функції-фабрики для створення довільних функційEmailFilter,
в залежності від потреб користувача.
Використання
існуючих функцій
Є
дві проблеми з поточним рішенням. Зпершу, є трохи дублікації в
предикатних фабриках-фукнціях вище, хоча я зпочатку казав, що
композитна природа функцій робить простим дотримуватись принципу
DRY. Так що покладемо край дублікації.
Щоб
зробити це для minimumSize таmaximumSize,
ми вводимо функцію sizeConstraint, що приймає предикат, що
перевіряє, чи розмір тіла листа в порядку. Цей розмір
буде переданий до предикату функцієюsizeConstraint:
Для
інших двох предикатів,sentByOneOf таnotSentByAnyOf,
ми збираємось ввести дуже загальну функцію вищого порядку, що
дозволяє нам виражати одну з двох функцій в термінах іншої.
Давайте
зреалізуємо функцію complement, що приймає предикат A
=> Boolean, та
повертає нову функцію, що завжди повертає протилежне наданого
предикату:
Тепер,
для існуючого предиката pми можемо отримати
компліментальну, викликавши complement(p).
Однак sentByAnyOfне є предикатом,
вона повертаєйого, точнішеEmailFilter.
Функції
Scala провадить дві композитні функції, що тепер допоможуть нам:
беручи дві функції, f таg,f.compose(g)повертає нову функцію,
що потім викликається, та спершу викликає g та потім застосовуємо f на її результаті.
Подібним чином f.andThen(g)поветає нову функцію,
що, коли викликається, буде застосовувати gдо результатуf.
Ми
можемо застосувати це для створення нашого предиката notSentByAnyOf
без дублікації кода:
Що
це означає, що ми просимо створити нову функцію, що спочатку
застосовує функціюsentByOneOfдо її аргументів (Set[String]),
та потім застосовує функцію complementдо предиката EmailFilter, що повертається попередньою
функцією. З використанням синтаксиса заміщувача Scala
для анонімних функцій, ми можемо записати це більш стисло:
Звичайно,
ви тепер помітите, що отримавши функцію complement,
ви можете також реалізовати предикат maximumSize в термінах minimumSize, замість виділення функціїsizeConstraint.
Однак, остання більш гнучка, дозволяючи вам задати довільні
перевірки розміру тіла листа.
Композиція
предикатів
Інша
проблема з нашим поштовим фільтром в тому, що ми наразі можемо
передавати тільки один EmailFilter до нашої функціїnewMailsForUser.
Звичайно, наші користувачі бажають сконфігурувати декілька
критеріїв. Нас треба спосіб створити композитний предикат, що
повертає true, коли або любий, жодний або
всі з предикатів, що він містить, повертає true.
Функція anyповертає новий
предикат, що, коли викликається з вводом a,
перевіряє, чи щонайменьше один з предикатів є true для значенняa.
Наша функція none просто повертає комплемент
предиката, що повертає any– якщо,
щонайменьше один предикат є true, умова для none не задовільняєтсья.
Нарешті, наша функція every робить, перевіряючи, що жодний
з комплементів до переданих йому предикатів true.
Тепер
ми можемо створити композитний EmailFilter, що представляє конфігурацію
користувача:
Як
інший приклад композиції функцій, знову розглянемо сценарій
нашого приклада. Як провайдер пошти, ми бажаємо не тільки
дозволити дозволити конфігурувати їх поштовий фільтр, але також
виконувати деяку обробку, що надсилається до нашого користувача.
Це прості функціїEmail
=> Email. Деякі можлві перетворення наступні:
123456789
valaddMissingSubject=(email:Email)=>if(email.subject.isEmpty)email.copy(subject="No subject")elseemailvalcheckSpelling=(email:Email)=>email.copy(text=email.text.replaceAll("your","you're"))valremoveInappropriateLanguage=(email:Email)=>email.copy(text=email.text.replaceAll("dynamic typing","**CENSORED**"))valaddAdvertismentToFooter=(email:Email)=>email.copy(text=email.text+"\nThis mail sent via Super Awesome Free Mail")
Тепер,
в залежності від погоди та настрою нашого босса, ми можемо
сконфігурувати наш конвеєр, або через виклики andThen,
або, маючи той самий ефект, використовуючи метод chain, визначений на на
об'єкті-компанйоні Function:
Я
не хочу тут занурюватись в деталі, але тепер, коли ми знаємо
більше щодо того, як ви можете скомпонувати або повторно
використати функції силами функцій вищого порядку, ви можете
знову повернутись до часткових функцій.
Сціплення
часткових функцій
В
главі про анонімні функції порівняння з шаблоном, я казав, що
часткові функції можуть використовуватись для створення гарної
альтернативи для зчеплення шаблону відповідальності: метод orElse, визначений на трейті PartialFunction дозволяє вам зціпити довільне
число часткових функцій, створюючи композитну часткову
функцію. Однак перша буде передавати керування наступній, тільки
якщо вона не визначена для наданого ввода. Таким чином ви можете
зробити щось подібне до наступного:
Також,
іноді PartialFunction не те, що нам треба.
Якщо ви думаєте про це, іншим шляхом представити факт, що
функція не визначена для всіх вхідних значень, є мати стандартну
функцію, чий тип результата є Option[A]– якщо функція не
визначена для вхідного значення, вона буде повертати None,
інакше Some[A].
Якщо
це не те, що нам треба в певному контексті, беручи PartialFunction
на ім'яpf,
ви можете викликатиpf.lift для отримати нормальної
функції, що повертає Option.
Якщо вам треба одне з пізніших, та треба часткова функція,
викличтеFunction.unlift(f).
Підсумок
В
цій статті ми побачили значення функцій вищого порядку, що
дозволяє вам повторно використати існуючі функції в нових,
непередбачуваних контекстах, та компонувати їх в дуже гнучкий
спосіб. Хоча в прикладах ви не зберегли багато в термінах рядків
кода, оскільки показані функції були скоріше крихітні, реальний
зиск є проілюструвати збільшення гнучкості. Також, компонування
та повторне використання функцій є дещо, що має вигоди не тільки
для малих функцій, але також на архитектурному рівні.
В
наступній статті ми продовжимо перевіряти шляхи для комбінації
функцій в розумінні застосування часткових фукнцій та карювання.